2023-06-14 08:52:36 +02:00
|
|
|
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
|
|
|
|
*
|
|
|
|
* SPDX-License-Identifier: Apache-2.0 */
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2021-10-24 14:19:19 +02:00
|
|
|
#include "scene/curves.h"
|
|
|
|
#include "scene/hair.h"
|
|
|
|
#include "scene/mesh.h"
|
|
|
|
#include "scene/object.h"
|
2021-12-01 17:30:46 +01:00
|
|
|
#include "scene/pointcloud.h"
|
2021-10-24 14:19:19 +02:00
|
|
|
#include "scene/volume.h"
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2021-10-24 14:19:19 +02:00
|
|
|
#include "blender/sync.h"
|
|
|
|
#include "blender/util.h"
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2021-10-24 14:19:19 +02:00
|
|
|
#include "util/foreach.h"
|
|
|
|
#include "util/task.h"
|
2020-03-11 17:59:32 +01:00
|
|
|
|
2020-02-02 13:09:18 +01:00
|
|
|
CCL_NAMESPACE_BEGIN
|
|
|
|
|
2021-09-06 18:22:24 +02:00
|
|
|
static Geometry::Type determine_geom_type(BObjectInfo &b_ob_info, bool use_particle_hair)
|
2020-08-19 15:46:50 +02:00
|
|
|
{
|
Curves: Rename "Hair" types, variables, and functions to "Curves"
Based on discussions from T95355 and T94193, the plan is to use
the name "Curves" to describe the data-block container for multiple
curves. Eventually this will replace the existing "Curve" data-block.
However, it will be a while before the curve data-block can be replaced
so in order to distinguish the two curve types in the UI, "Hair Curves"
will be used, but eventually changed back to "Curves".
This patch renames "hair-related" files, functions, types, and variable
names to this convention. A deep rename is preferred to keep code
consistent and to avoid any "hair" terminology from leaking, since the
new data-block is meant for all curve types, not just hair use cases.
The downside of this naming is that the difference between "Curve"
and "Curves" has become important. That was considered during
design discussons and deemed acceptable, especially given the
non-permanent nature of the somewhat common conflict.
Some points of interest:
- All DNA compatibility is lost, just like rBf59767ff9729.
- I renamed `ID_HA` to `ID_CV` so there is no complete mismatch.
- `hair_curves` is used where necessary to distinguish from the
existing "curves" plural.
- I didn't rename any of the cycles/rendering code function names,
since that is also used by the old hair particle system.
Differential Revision: https://developer.blender.org/D14007
2022-02-07 18:55:54 +01:00
|
|
|
if (b_ob_info.object_data.is_a(&RNA_Curves) || use_particle_hair) {
|
2020-08-19 15:46:50 +02:00
|
|
|
return Geometry::HAIR;
|
|
|
|
}
|
|
|
|
|
2021-12-01 17:30:46 +01:00
|
|
|
if (b_ob_info.object_data.is_a(&RNA_PointCloud)) {
|
|
|
|
return Geometry::POINTCLOUD;
|
|
|
|
}
|
|
|
|
|
2021-09-06 18:22:24 +02:00
|
|
|
if (b_ob_info.object_data.is_a(&RNA_Volume) ||
|
|
|
|
(b_ob_info.object_data == b_ob_info.real_object.data() &&
|
|
|
|
object_fluid_gas_domain_find(b_ob_info.real_object)))
|
|
|
|
{
|
2020-08-19 15:46:50 +02:00
|
|
|
return Geometry::VOLUME;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Geometry::MESH;
|
|
|
|
}
|
|
|
|
|
2021-01-25 14:47:56 +01:00
|
|
|
array<Node *> BlenderSync::find_used_shaders(BL::Object &b_ob)
|
2020-02-02 13:09:18 +01:00
|
|
|
{
|
|
|
|
BL::Material material_override = view_layer.material_override;
|
2020-03-17 16:52:14 +01:00
|
|
|
Shader *default_shader = (b_ob.type() == BL::Object::type_VOLUME) ? scene->default_volume :
|
|
|
|
scene->default_surface;
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2020-11-04 11:17:38 +01:00
|
|
|
array<Node *> used_shaders;
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2021-01-25 16:20:10 +01:00
|
|
|
for (BL::MaterialSlot &b_slot : b_ob.material_slots) {
|
2020-02-02 13:09:18 +01:00
|
|
|
if (material_override) {
|
|
|
|
find_shader(material_override, used_shaders, default_shader);
|
|
|
|
}
|
|
|
|
else {
|
2021-01-25 16:20:10 +01:00
|
|
|
BL::ID b_material(b_slot.material());
|
2020-02-02 13:09:18 +01:00
|
|
|
find_shader(b_material, used_shaders, default_shader);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (used_shaders.size() == 0) {
|
2023-09-17 01:01:48 +02:00
|
|
|
if (material_override) {
|
2020-02-02 13:09:18 +01:00
|
|
|
find_shader(material_override, used_shaders, default_shader);
|
2023-09-17 01:01:48 +02:00
|
|
|
}
|
|
|
|
else {
|
2020-11-04 11:17:38 +01:00
|
|
|
used_shaders.push_back_slow(default_shader);
|
2023-09-17 01:01:48 +02:00
|
|
|
}
|
2020-02-02 13:09:18 +01:00
|
|
|
}
|
|
|
|
|
2021-01-25 14:47:56 +01:00
|
|
|
return used_shaders;
|
|
|
|
}
|
|
|
|
|
|
|
|
Geometry *BlenderSync::sync_geometry(BL::Depsgraph &b_depsgraph,
|
2021-09-06 18:22:24 +02:00
|
|
|
BObjectInfo &b_ob_info,
|
2021-01-25 14:47:56 +01:00
|
|
|
bool object_updated,
|
|
|
|
bool use_particle_hair,
|
|
|
|
TaskPool *task_pool)
|
|
|
|
{
|
|
|
|
/* Test if we can instance or if the object is modified. */
|
2021-09-06 18:22:24 +02:00
|
|
|
Geometry::Type geom_type = determine_geom_type(b_ob_info, use_particle_hair);
|
2021-09-27 18:52:09 +02:00
|
|
|
BL::ID b_key_id = (b_ob_info.is_real_object_data() &&
|
|
|
|
BKE_object_is_modified(b_ob_info.real_object)) ?
|
|
|
|
b_ob_info.real_object :
|
|
|
|
b_ob_info.object_data;
|
2021-09-22 15:53:04 +02:00
|
|
|
GeometryKey key(b_key_id.ptr.data, geom_type);
|
2021-01-25 14:47:56 +01:00
|
|
|
|
|
|
|
/* Find shader indices. */
|
2021-09-06 18:22:24 +02:00
|
|
|
array<Node *> used_shaders = find_used_shaders(b_ob_info.iter_object);
|
2021-01-25 14:47:56 +01:00
|
|
|
|
2020-10-21 16:38:36 +02:00
|
|
|
/* Ensure we only sync instanced geometry once. */
|
2020-02-02 12:04:19 +01:00
|
|
|
Geometry *geom = geometry_map.find(key);
|
2020-10-21 16:38:36 +02:00
|
|
|
if (geom) {
|
|
|
|
if (geometry_synced.find(geom) != geometry_synced.end()) {
|
|
|
|
return geom;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Test if we need to sync. */
|
2020-02-02 12:04:19 +01:00
|
|
|
bool sync = true;
|
|
|
|
if (geom == NULL) {
|
|
|
|
/* Add new geometry if it did not exist yet. */
|
|
|
|
if (geom_type == Geometry::HAIR) {
|
Cycles: introduce an ownership system to protect nodes from unwanted deletions.
Problem: the Blender synchronization process creates and tags nodes for usage. It does
this by directly adding and removing nodes from the scene data. If some node is not tagged
as used at the end of a synchronization, it then deletes the node from the scene. This poses
a problem when it comes to supporting procedural nodes who can create other nodes not known
by the Blender synchonization system, which will remove them.
Nodes now have a NodeOwner, which is set after creation. Those owners for now are the Scene
for scene level nodes and ShaderGraph for shader nodes. Instead of creating and deleting
nodes using `new` and `delete` explicitely, we now use `create_node` and `delete_node` methods
found on the owners. `delete_node` will assert that the owner is the right one.
Whenever a scene level node is created or deleted, the appropriate node manager is tagged for
an update, freeing this responsability from BlenderSync or other software exporters.
Concerning BlenderSync, the `id_maps` do not explicitely manipulate scene data anymore, they
only keep track of which nodes are used, employing the scene to create and delete them. To
achieve this, the ParticleSystem is now a Node, although it does not have any sockets.
This is part of T79131.
Reviewed By: #cycles, brecht
Maniphest Tasks: T79131
Differential Revision: https://developer.blender.org/D8540
2020-08-30 23:20:51 +02:00
|
|
|
geom = scene->create_node<Hair>();
|
2020-02-02 12:04:19 +01:00
|
|
|
}
|
2020-08-19 15:46:50 +02:00
|
|
|
else if (geom_type == Geometry::VOLUME) {
|
Cycles: introduce an ownership system to protect nodes from unwanted deletions.
Problem: the Blender synchronization process creates and tags nodes for usage. It does
this by directly adding and removing nodes from the scene data. If some node is not tagged
as used at the end of a synchronization, it then deletes the node from the scene. This poses
a problem when it comes to supporting procedural nodes who can create other nodes not known
by the Blender synchonization system, which will remove them.
Nodes now have a NodeOwner, which is set after creation. Those owners for now are the Scene
for scene level nodes and ShaderGraph for shader nodes. Instead of creating and deleting
nodes using `new` and `delete` explicitely, we now use `create_node` and `delete_node` methods
found on the owners. `delete_node` will assert that the owner is the right one.
Whenever a scene level node is created or deleted, the appropriate node manager is tagged for
an update, freeing this responsability from BlenderSync or other software exporters.
Concerning BlenderSync, the `id_maps` do not explicitely manipulate scene data anymore, they
only keep track of which nodes are used, employing the scene to create and delete them. To
achieve this, the ParticleSystem is now a Node, although it does not have any sockets.
This is part of T79131.
Reviewed By: #cycles, brecht
Maniphest Tasks: T79131
Differential Revision: https://developer.blender.org/D8540
2020-08-30 23:20:51 +02:00
|
|
|
geom = scene->create_node<Volume>();
|
2020-08-19 15:46:50 +02:00
|
|
|
}
|
2021-12-01 17:30:46 +01:00
|
|
|
else if (geom_type == Geometry::POINTCLOUD) {
|
|
|
|
geom = scene->create_node<PointCloud>();
|
|
|
|
}
|
2020-02-02 12:04:19 +01:00
|
|
|
else {
|
Cycles: introduce an ownership system to protect nodes from unwanted deletions.
Problem: the Blender synchronization process creates and tags nodes for usage. It does
this by directly adding and removing nodes from the scene data. If some node is not tagged
as used at the end of a synchronization, it then deletes the node from the scene. This poses
a problem when it comes to supporting procedural nodes who can create other nodes not known
by the Blender synchonization system, which will remove them.
Nodes now have a NodeOwner, which is set after creation. Those owners for now are the Scene
for scene level nodes and ShaderGraph for shader nodes. Instead of creating and deleting
nodes using `new` and `delete` explicitely, we now use `create_node` and `delete_node` methods
found on the owners. `delete_node` will assert that the owner is the right one.
Whenever a scene level node is created or deleted, the appropriate node manager is tagged for
an update, freeing this responsability from BlenderSync or other software exporters.
Concerning BlenderSync, the `id_maps` do not explicitely manipulate scene data anymore, they
only keep track of which nodes are used, employing the scene to create and delete them. To
achieve this, the ParticleSystem is now a Node, although it does not have any sockets.
This is part of T79131.
Reviewed By: #cycles, brecht
Maniphest Tasks: T79131
Differential Revision: https://developer.blender.org/D8540
2020-08-30 23:20:51 +02:00
|
|
|
geom = scene->create_node<Mesh>();
|
2020-02-02 12:04:19 +01:00
|
|
|
}
|
|
|
|
geometry_map.add(key, geom);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
/* Test if we need to update existing geometry. */
|
2021-09-22 15:53:04 +02:00
|
|
|
sync = geometry_map.update(geom, b_key_id);
|
2020-02-02 12:04:19 +01:00
|
|
|
}
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2020-02-02 12:04:19 +01:00
|
|
|
if (!sync) {
|
|
|
|
/* If transform was applied to geometry, need full update. */
|
|
|
|
if (object_updated && geom->transform_applied) {
|
2020-02-02 13:09:18 +01:00
|
|
|
;
|
2020-02-02 12:04:19 +01:00
|
|
|
}
|
|
|
|
/* Test if shaders changed, these can be object level so geometry
|
2020-02-02 13:09:18 +01:00
|
|
|
* does not get tagged for recalc. */
|
2020-11-04 11:17:38 +01:00
|
|
|
else if (geom->get_used_shaders() != used_shaders) {
|
2020-02-02 13:09:18 +01:00
|
|
|
;
|
2020-02-02 12:04:19 +01:00
|
|
|
}
|
2020-02-02 13:09:18 +01:00
|
|
|
else {
|
|
|
|
/* Even if not tagged for recalc, we may need to sync anyway
|
2020-02-02 12:04:19 +01:00
|
|
|
* because the shader needs different geometry attributes. */
|
2020-02-02 13:09:18 +01:00
|
|
|
bool attribute_recalc = false;
|
|
|
|
|
2020-11-04 11:17:38 +01:00
|
|
|
foreach (Node *node, geom->get_used_shaders()) {
|
|
|
|
Shader *shader = static_cast<Shader *>(node);
|
Cycles: optimize device updates
This optimizes device updates (during user edits or frame changes in
the viewport) by avoiding unnecessary computations. To achieve this,
we use a combination of the sockets' update flags as well as some new
flags passed to the various managers when tagging for an update to tell
exactly what the tagging is for (e.g. shader was modified, object was
removed, etc.).
Besides avoiding recomputations, we also avoid resending to the devices
unmodified data arrays, thus reducing bandwidth usage. For OptiX and
Embree, BVH packing was also multithreaded.
The performance improvements may vary depending on the used device (CPU
or GPU), and the content of the scene. Simple scenes (e.g. with no adaptive
subdivision or volumes) rendered using OptiX will benefit from this work
the most.
On average, for a variety of animated scenes, this gives a 3x speedup.
Reviewed By: #cycles, brecht
Maniphest Tasks: T79174
Differential Revision: https://developer.blender.org/D9555
2021-01-22 15:01:26 +01:00
|
|
|
if (shader->need_update_geometry()) {
|
2020-02-02 13:09:18 +01:00
|
|
|
attribute_recalc = true;
|
2020-02-02 12:04:19 +01:00
|
|
|
}
|
|
|
|
}
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2020-02-02 12:04:19 +01:00
|
|
|
if (!attribute_recalc) {
|
|
|
|
return geom;
|
|
|
|
}
|
2020-02-02 13:09:18 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-02 12:04:19 +01:00
|
|
|
geometry_synced.insert(geom);
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2021-09-06 18:22:24 +02:00
|
|
|
geom->name = ustring(b_ob_info.object_data.name().c_str());
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2020-10-31 17:21:07 +01:00
|
|
|
/* Store the shaders immediately for the object attribute code. */
|
2020-11-04 11:17:38 +01:00
|
|
|
geom->set_used_shaders(used_shaders);
|
2020-10-31 17:21:07 +01:00
|
|
|
|
2020-10-21 16:38:36 +02:00
|
|
|
auto sync_func = [=]() mutable {
|
2023-09-17 01:01:48 +02:00
|
|
|
if (progress.get_cancel()) {
|
2020-10-21 16:38:36 +02:00
|
|
|
return;
|
2023-09-17 01:01:48 +02:00
|
|
|
}
|
2020-10-21 16:38:36 +02:00
|
|
|
|
2021-09-06 18:22:24 +02:00
|
|
|
progress.set_sync_status("Synchronizing object", b_ob_info.real_object.name());
|
2020-10-21 16:38:36 +02:00
|
|
|
|
|
|
|
if (geom_type == Geometry::HAIR) {
|
|
|
|
Hair *hair = static_cast<Hair *>(geom);
|
2021-09-06 18:22:24 +02:00
|
|
|
sync_hair(b_depsgraph, b_ob_info, hair);
|
2020-10-21 16:38:36 +02:00
|
|
|
}
|
|
|
|
else if (geom_type == Geometry::VOLUME) {
|
|
|
|
Volume *volume = static_cast<Volume *>(geom);
|
2021-09-06 18:22:24 +02:00
|
|
|
sync_volume(b_ob_info, volume);
|
2020-10-21 16:38:36 +02:00
|
|
|
}
|
2021-12-01 17:30:46 +01:00
|
|
|
else if (geom_type == Geometry::POINTCLOUD) {
|
|
|
|
PointCloud *pointcloud = static_cast<PointCloud *>(geom);
|
|
|
|
sync_pointcloud(pointcloud, b_ob_info);
|
|
|
|
}
|
2020-10-21 16:38:36 +02:00
|
|
|
else {
|
|
|
|
Mesh *mesh = static_cast<Mesh *>(geom);
|
2021-09-06 18:22:24 +02:00
|
|
|
sync_mesh(b_depsgraph, b_ob_info, mesh);
|
2020-10-21 16:38:36 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Defer the actual geometry sync to the task_pool for multithreading */
|
|
|
|
if (task_pool) {
|
|
|
|
task_pool->push(sync_func);
|
2020-02-02 13:09:18 +01:00
|
|
|
}
|
|
|
|
else {
|
2020-10-21 16:38:36 +02:00
|
|
|
sync_func();
|
2020-02-02 13:09:18 +01:00
|
|
|
}
|
|
|
|
|
2020-02-02 12:04:19 +01:00
|
|
|
return geom;
|
2020-02-02 13:09:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void BlenderSync::sync_geometry_motion(BL::Depsgraph &b_depsgraph,
|
2021-09-06 18:22:24 +02:00
|
|
|
BObjectInfo &b_ob_info,
|
2020-02-02 13:09:18 +01:00
|
|
|
Object *object,
|
|
|
|
float motion_time,
|
2020-10-21 16:38:36 +02:00
|
|
|
bool use_particle_hair,
|
|
|
|
TaskPool *task_pool)
|
2020-02-02 13:09:18 +01:00
|
|
|
{
|
2020-02-02 12:04:19 +01:00
|
|
|
/* Ensure we only sync instanced geometry once. */
|
2020-11-04 11:17:38 +01:00
|
|
|
Geometry *geom = object->get_geometry();
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2021-09-09 17:22:20 +02:00
|
|
|
if (geometry_motion_synced.find(geom) != geometry_motion_synced.end() ||
|
|
|
|
geometry_motion_attribute_synced.find(geom) != geometry_motion_attribute_synced.end())
|
|
|
|
{
|
2020-02-02 13:09:18 +01:00
|
|
|
return;
|
2021-09-09 17:22:20 +02:00
|
|
|
}
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2020-02-02 12:04:19 +01:00
|
|
|
geometry_motion_synced.insert(geom);
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2020-02-02 12:04:19 +01:00
|
|
|
/* Ensure we only motion sync geometry that also had geometry synced, to avoid
|
2020-02-02 13:09:18 +01:00
|
|
|
* unnecessary work and to ensure that its attributes were clear. */
|
2023-09-17 01:01:48 +02:00
|
|
|
if (geometry_synced.find(geom) == geometry_synced.end()) {
|
2020-02-02 13:09:18 +01:00
|
|
|
return;
|
2023-09-17 01:01:48 +02:00
|
|
|
}
|
2020-02-02 13:09:18 +01:00
|
|
|
|
2020-02-02 12:04:19 +01:00
|
|
|
/* Find time matching motion step required by geometry. */
|
|
|
|
int motion_step = geom->motion_step(motion_time);
|
2020-02-02 13:09:18 +01:00
|
|
|
if (motion_step < 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-21 16:38:36 +02:00
|
|
|
auto sync_func = [=]() mutable {
|
2023-09-17 01:01:48 +02:00
|
|
|
if (progress.get_cancel()) {
|
2020-10-21 16:38:36 +02:00
|
|
|
return;
|
2023-09-17 01:01:48 +02:00
|
|
|
}
|
2020-10-21 16:38:36 +02:00
|
|
|
|
Curves: Rename "Hair" types, variables, and functions to "Curves"
Based on discussions from T95355 and T94193, the plan is to use
the name "Curves" to describe the data-block container for multiple
curves. Eventually this will replace the existing "Curve" data-block.
However, it will be a while before the curve data-block can be replaced
so in order to distinguish the two curve types in the UI, "Hair Curves"
will be used, but eventually changed back to "Curves".
This patch renames "hair-related" files, functions, types, and variable
names to this convention. A deep rename is preferred to keep code
consistent and to avoid any "hair" terminology from leaking, since the
new data-block is meant for all curve types, not just hair use cases.
The downside of this naming is that the difference between "Curve"
and "Curves" has become important. That was considered during
design discussons and deemed acceptable, especially given the
non-permanent nature of the somewhat common conflict.
Some points of interest:
- All DNA compatibility is lost, just like rBf59767ff9729.
- I renamed `ID_HA` to `ID_CV` so there is no complete mismatch.
- `hair_curves` is used where necessary to distinguish from the
existing "curves" plural.
- I didn't rename any of the cycles/rendering code function names,
since that is also used by the old hair particle system.
Differential Revision: https://developer.blender.org/D14007
2022-02-07 18:55:54 +01:00
|
|
|
if (b_ob_info.object_data.is_a(&RNA_Curves) || use_particle_hair) {
|
2020-10-21 16:38:36 +02:00
|
|
|
Hair *hair = static_cast<Hair *>(geom);
|
2021-09-06 18:22:24 +02:00
|
|
|
sync_hair_motion(b_depsgraph, b_ob_info, hair, motion_step);
|
2020-10-21 16:38:36 +02:00
|
|
|
}
|
2021-09-06 18:22:24 +02:00
|
|
|
else if (b_ob_info.object_data.is_a(&RNA_Volume) ||
|
|
|
|
object_fluid_gas_domain_find(b_ob_info.real_object))
|
|
|
|
{
|
2020-10-21 16:38:36 +02:00
|
|
|
/* No volume motion blur support yet. */
|
|
|
|
}
|
2021-12-01 17:30:46 +01:00
|
|
|
else if (b_ob_info.object_data.is_a(&RNA_PointCloud)) {
|
|
|
|
PointCloud *pointcloud = static_cast<PointCloud *>(geom);
|
|
|
|
sync_pointcloud_motion(pointcloud, b_ob_info, motion_step);
|
|
|
|
}
|
2020-10-21 16:38:36 +02:00
|
|
|
else {
|
|
|
|
Mesh *mesh = static_cast<Mesh *>(geom);
|
2021-09-06 18:22:24 +02:00
|
|
|
sync_mesh_motion(b_depsgraph, b_ob_info, mesh, motion_step);
|
2020-10-21 16:38:36 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Defer the actual geometry sync to the task_pool for multithreading */
|
|
|
|
if (task_pool) {
|
|
|
|
task_pool->push(sync_func);
|
2020-02-02 13:09:18 +01:00
|
|
|
}
|
|
|
|
else {
|
2020-10-21 16:38:36 +02:00
|
|
|
sync_func();
|
2020-02-02 13:09:18 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
CCL_NAMESPACE_END
|