mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
Add multithreaded mesh to RenderMesh conversion in Tydra
When TINYUSDZ_ENABLE_THREAD is enabled, mesh conversion now uses a two-phase parallel approach: 1. Material pre-collection: Single-threaded pass collects all bound materials and converts them first, building a read-only cache 2. Parallel mesh conversion: Meshes are converted in parallel using worker threads with lock-free access to the material cache This reduces conversion time for scenes with multiple objects by utilizing multiple CPU cores for the expensive mesh conversion step. Configuration options added to RenderSceneConverterConfig: - enable_parallel_mesh_conversion: Enable/disable parallel conversion - num_mesh_conversion_threads: Number of worker threads (0 = auto) - min_meshes_for_parallel: Minimum meshes to trigger parallel (default: 4) - mesh_task_queue_capacity: Task queue capacity Also adds geomsubset-merge-test.usda for testing GeomSubset material grouping with multiple subsets sharing the same material. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
194
models/geomsubset-merge-test.usda
Normal file
194
models/geomsubset-merge-test.usda
Normal file
@@ -0,0 +1,194 @@
|
||||
#usda 1.0
|
||||
(
|
||||
defaultPrim = "root"
|
||||
doc = "Test for GeomSubset merging with same material"
|
||||
metersPerUnit = 1
|
||||
upAxis = "Y"
|
||||
)
|
||||
|
||||
# Test case: A 3x3 grid of quads where multiple GeomSubsets share the same material
|
||||
# 6 GeomSubsets total: 2 red, 2 green, 2 blue -> should merge to 3 submeshes
|
||||
|
||||
def Xform "root"
|
||||
{
|
||||
def Mesh "grid_mesh" (
|
||||
prepend apiSchemas = ["MaterialBindingAPI"]
|
||||
)
|
||||
{
|
||||
uniform bool doubleSided = 1
|
||||
float3[] extent = [(-1.5, 0, -1.5), (1.5, 0, 1.5)]
|
||||
|
||||
# 9 quads in a 3x3 grid
|
||||
int[] faceVertexCounts = [4, 4, 4, 4, 4, 4, 4, 4, 4]
|
||||
|
||||
# Each quad is CCW when viewed from +Y
|
||||
# Grid layout (face indices):
|
||||
# 0 1 2
|
||||
# 3 4 5
|
||||
# 6 7 8
|
||||
int[] faceVertexIndices = [
|
||||
0, 1, 5, 4, # Face 0 (top-left)
|
||||
1, 2, 6, 5, # Face 1 (top-center)
|
||||
2, 3, 7, 6, # Face 2 (top-right)
|
||||
4, 5, 9, 8, # Face 3 (middle-left)
|
||||
5, 6, 10, 9, # Face 4 (center)
|
||||
6, 7, 11, 10, # Face 5 (middle-right)
|
||||
8, 9, 13, 12, # Face 6 (bottom-left)
|
||||
9, 10, 14, 13, # Face 7 (bottom-center)
|
||||
10, 11, 15, 14 # Face 8 (bottom-right)
|
||||
]
|
||||
|
||||
# 4x4 grid of vertices
|
||||
point3f[] points = [
|
||||
(-1.5, 0, -1.5), (-0.5, 0, -1.5), (0.5, 0, -1.5), (1.5, 0, -1.5),
|
||||
(-1.5, 0, -0.5), (-0.5, 0, -0.5), (0.5, 0, -0.5), (1.5, 0, -0.5),
|
||||
(-1.5, 0, 0.5), (-0.5, 0, 0.5), (0.5, 0, 0.5), (1.5, 0, 0.5),
|
||||
(-1.5, 0, 1.5), (-0.5, 0, 1.5), (0.5, 0, 1.5), (1.5, 0, 1.5)
|
||||
]
|
||||
|
||||
# Face-varying normals (all pointing up)
|
||||
normal3f[] normals = [
|
||||
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), # Face 0
|
||||
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), # Face 1
|
||||
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), # Face 2
|
||||
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), # Face 3
|
||||
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), # Face 4
|
||||
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), # Face 5
|
||||
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), # Face 6
|
||||
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), # Face 7
|
||||
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0) # Face 8
|
||||
] (
|
||||
interpolation = "faceVarying"
|
||||
)
|
||||
|
||||
# UV coordinates per face vertex
|
||||
texCoord2f[] primvars:st = [
|
||||
(0, 0), (0.333, 0), (0.333, 0.333), (0, 0.333), # Face 0
|
||||
(0.333, 0), (0.666, 0), (0.666, 0.333), (0.333, 0.333), # Face 1
|
||||
(0.666, 0), (1, 0), (1, 0.333), (0.666, 0.333), # Face 2
|
||||
(0, 0.333), (0.333, 0.333), (0.333, 0.666), (0, 0.666), # Face 3
|
||||
(0.333, 0.333), (0.666, 0.333), (0.666, 0.666), (0.333, 0.666), # Face 4
|
||||
(0.666, 0.333), (1, 0.333), (1, 0.666), (0.666, 0.666), # Face 5
|
||||
(0, 0.666), (0.333, 0.666), (0.333, 1), (0, 1), # Face 6
|
||||
(0.333, 0.666), (0.666, 0.666), (0.666, 1), (0.333, 1), # Face 7
|
||||
(0.666, 0.666), (1, 0.666), (1, 1), (0.666, 1) # Face 8
|
||||
] (
|
||||
interpolation = "faceVarying"
|
||||
)
|
||||
|
||||
uniform token subdivisionScheme = "none"
|
||||
uniform token subsetFamily:materialBind:familyType = "partition"
|
||||
|
||||
# Pattern: checkerboard-like with 3 colors
|
||||
# Red: faces 0, 4, 8 (diagonal)
|
||||
# Green: faces 1, 3, 5, 7 (cross)
|
||||
# Blue: faces 2, 6 (corners)
|
||||
|
||||
# Split each material into 2 GeomSubsets to test merging
|
||||
|
||||
# RED material - split into 2 subsets
|
||||
def GeomSubset "red_subset_A" (
|
||||
prepend apiSchemas = ["MaterialBindingAPI"]
|
||||
)
|
||||
{
|
||||
uniform token elementType = "face"
|
||||
uniform token familyName = "materialBind"
|
||||
int[] indices = [0, 8] # Non-contiguous faces
|
||||
rel material:binding = </root/Materials/Red>
|
||||
}
|
||||
|
||||
def GeomSubset "red_subset_B" (
|
||||
prepend apiSchemas = ["MaterialBindingAPI"]
|
||||
)
|
||||
{
|
||||
uniform token elementType = "face"
|
||||
uniform token familyName = "materialBind"
|
||||
int[] indices = [4] # Center face
|
||||
rel material:binding = </root/Materials/Red>
|
||||
}
|
||||
|
||||
# GREEN material - split into 2 subsets
|
||||
def GeomSubset "green_subset_A" (
|
||||
prepend apiSchemas = ["MaterialBindingAPI"]
|
||||
)
|
||||
{
|
||||
uniform token elementType = "face"
|
||||
uniform token familyName = "materialBind"
|
||||
int[] indices = [1, 7] # Top-center and bottom-center
|
||||
rel material:binding = </root/Materials/Green>
|
||||
}
|
||||
|
||||
def GeomSubset "green_subset_B" (
|
||||
prepend apiSchemas = ["MaterialBindingAPI"]
|
||||
)
|
||||
{
|
||||
uniform token elementType = "face"
|
||||
uniform token familyName = "materialBind"
|
||||
int[] indices = [3, 5] # Middle-left and middle-right
|
||||
rel material:binding = </root/Materials/Green>
|
||||
}
|
||||
|
||||
# BLUE material - split into 2 subsets
|
||||
def GeomSubset "blue_subset_A" (
|
||||
prepend apiSchemas = ["MaterialBindingAPI"]
|
||||
)
|
||||
{
|
||||
uniform token elementType = "face"
|
||||
uniform token familyName = "materialBind"
|
||||
int[] indices = [2] # Top-right corner
|
||||
rel material:binding = </root/Materials/Blue>
|
||||
}
|
||||
|
||||
def GeomSubset "blue_subset_B" (
|
||||
prepend apiSchemas = ["MaterialBindingAPI"]
|
||||
)
|
||||
{
|
||||
uniform token elementType = "face"
|
||||
uniform token familyName = "materialBind"
|
||||
int[] indices = [6] # Bottom-left corner
|
||||
rel material:binding = </root/Materials/Blue>
|
||||
}
|
||||
}
|
||||
|
||||
def Scope "Materials"
|
||||
{
|
||||
def Material "Red"
|
||||
{
|
||||
token outputs:surface.connect = </root/Materials/Red/Shader.outputs:surface>
|
||||
|
||||
def Shader "Shader"
|
||||
{
|
||||
uniform token info:id = "UsdPreviewSurface"
|
||||
color3f inputs:diffuseColor = (1, 0, 0)
|
||||
float inputs:roughness = 0.5
|
||||
token outputs:surface
|
||||
}
|
||||
}
|
||||
|
||||
def Material "Green"
|
||||
{
|
||||
token outputs:surface.connect = </root/Materials/Green/Shader.outputs:surface>
|
||||
|
||||
def Shader "Shader"
|
||||
{
|
||||
uniform token info:id = "UsdPreviewSurface"
|
||||
color3f inputs:diffuseColor = (0, 1, 0)
|
||||
float inputs:roughness = 0.5
|
||||
token outputs:surface
|
||||
}
|
||||
}
|
||||
|
||||
def Material "Blue"
|
||||
{
|
||||
token outputs:surface.connect = </root/Materials/Blue/Shader.outputs:surface>
|
||||
|
||||
def Shader "Shader"
|
||||
{
|
||||
uniform token info:id = "UsdPreviewSurface"
|
||||
color3f inputs:diffuseColor = (0, 0, 1)
|
||||
float inputs:roughness = 0.5
|
||||
token outputs:surface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,6 +130,14 @@
|
||||
#include "tydra/scene-access.hh"
|
||||
#include "tydra/shader-network.hh"
|
||||
|
||||
#if defined(TINYUSDZ_ENABLE_THREAD)
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include "task-queue.hh"
|
||||
#endif // TINYUSDZ_ENABLE_THREAD
|
||||
|
||||
namespace tinyusdz {
|
||||
|
||||
namespace tydra {
|
||||
@@ -7461,6 +7469,265 @@ struct MeshVisitorEnv {
|
||||
const RenderSceneConverterEnv *env{nullptr};
|
||||
};
|
||||
|
||||
#if defined(TINYUSDZ_ENABLE_THREAD)
|
||||
|
||||
//
|
||||
// Data structure for parallel mesh conversion
|
||||
//
|
||||
|
||||
// Holds bound material information for a mesh/subset
|
||||
struct BoundMaterialInfo {
|
||||
std::string material_path;
|
||||
std::string backface_material_path;
|
||||
const tinyusdz::Material *material{nullptr};
|
||||
const tinyusdz::Material *backface_material{nullptr};
|
||||
};
|
||||
|
||||
// Information collected during material pre-collection pass
|
||||
struct MeshMaterialInfo {
|
||||
Path abs_path;
|
||||
const Prim *prim{nullptr};
|
||||
|
||||
// Mesh-level material binding
|
||||
BoundMaterialInfo mesh_material;
|
||||
|
||||
// GeomSubset material bindings (key = subset prim name)
|
||||
std::map<std::string, BoundMaterialInfo> subset_materials;
|
||||
|
||||
// Type of geometry
|
||||
enum class GeomType {
|
||||
Mesh,
|
||||
Cube,
|
||||
Sphere
|
||||
};
|
||||
GeomType geom_type{GeomType::Mesh};
|
||||
};
|
||||
|
||||
// Environment for material collection pass
|
||||
struct MaterialCollectorEnv {
|
||||
RenderSceneConverter *converter{nullptr};
|
||||
const RenderSceneConverterEnv *env{nullptr};
|
||||
|
||||
// Output: collected mesh material info (for parallel mesh conversion)
|
||||
std::vector<MeshMaterialInfo> mesh_infos;
|
||||
|
||||
// Set of unique material paths to convert
|
||||
std::set<std::string> material_paths;
|
||||
|
||||
// Map from material path to (Material*, backface purpose)
|
||||
std::map<std::string, const tinyusdz::Material *> material_ptr_map;
|
||||
};
|
||||
|
||||
// Visitor that only collects materials without converting meshes
|
||||
bool MaterialCollectorVisitor(const tinyusdz::Path &abs_path,
|
||||
const tinyusdz::Prim &prim,
|
||||
const int32_t level, void *userdata,
|
||||
std::string *err) {
|
||||
if (!userdata) {
|
||||
if (err) {
|
||||
(*err) += "userdata pointer must be filled.";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
MaterialCollectorEnv *collectorEnv =
|
||||
reinterpret_cast<MaterialCollectorEnv *>(userdata);
|
||||
|
||||
if (level > 1024 * 1024) {
|
||||
if (err) {
|
||||
(*err) += "Scene graph is too deep.\n";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Lambda to collect bound material info
|
||||
auto CollectBoundMaterial = [&](const Path &prim_path,
|
||||
const std::string &purpose,
|
||||
BoundMaterialInfo &out_info) {
|
||||
tinyusdz::Path bound_material_path;
|
||||
const tinyusdz::Material *bound_material{nullptr};
|
||||
|
||||
bool ret = tinyusdz::tydra::GetBoundMaterial(
|
||||
collectorEnv->env->stage, prim_path, purpose,
|
||||
&bound_material_path, &bound_material, err);
|
||||
|
||||
if (ret && bound_material) {
|
||||
std::string path_str = bound_material_path.full_path_name();
|
||||
if (purpose.empty()) {
|
||||
out_info.material_path = path_str;
|
||||
out_info.material = bound_material;
|
||||
} else {
|
||||
out_info.backface_material_path = path_str;
|
||||
out_info.backface_material = bound_material;
|
||||
}
|
||||
|
||||
// Record unique material paths
|
||||
collectorEnv->material_paths.insert(path_str);
|
||||
collectorEnv->material_ptr_map[path_str] = bound_material;
|
||||
}
|
||||
};
|
||||
|
||||
// Process mesh geometry
|
||||
if (const tinyusdz::GeomMesh *pmesh = prim.as<tinyusdz::GeomMesh>()) {
|
||||
if (!pmesh->points.authored()) {
|
||||
return true; // Skip meshes without points
|
||||
}
|
||||
|
||||
MeshMaterialInfo info;
|
||||
info.abs_path = abs_path;
|
||||
info.prim = &prim;
|
||||
info.geom_type = MeshMaterialInfo::GeomType::Mesh;
|
||||
|
||||
// Collect mesh-level material binding
|
||||
CollectBoundMaterial(abs_path, "", info.mesh_material);
|
||||
|
||||
std::string backface_purpose =
|
||||
collectorEnv->env->material_config.default_backface_material_purpose_name;
|
||||
if (!backface_purpose.empty() &&
|
||||
pmesh->has_materialBinding(value::token(backface_purpose))) {
|
||||
CollectBoundMaterial(abs_path, backface_purpose, info.mesh_material);
|
||||
}
|
||||
|
||||
// Collect GeomSubset material bindings
|
||||
std::vector<const GeomSubset *> material_subsets =
|
||||
GetMaterialBindGeomSubsets(prim);
|
||||
|
||||
for (const auto &psubset : material_subsets) {
|
||||
Path subset_abs_path = abs_path.AppendElement(psubset->name);
|
||||
BoundMaterialInfo subset_info;
|
||||
|
||||
CollectBoundMaterial(subset_abs_path, "", subset_info);
|
||||
|
||||
if (!backface_purpose.empty() &&
|
||||
psubset->has_materialBinding(value::token(backface_purpose))) {
|
||||
CollectBoundMaterial(subset_abs_path, backface_purpose, subset_info);
|
||||
}
|
||||
|
||||
info.subset_materials[psubset->name] = subset_info;
|
||||
}
|
||||
|
||||
collectorEnv->mesh_infos.push_back(std::move(info));
|
||||
}
|
||||
|
||||
// Process cube geometry
|
||||
if (const tinyusdz::GeomCube *pcube = prim.as<tinyusdz::GeomCube>()) {
|
||||
(void)pcube; // Used for type check only
|
||||
MeshMaterialInfo info;
|
||||
info.abs_path = abs_path;
|
||||
info.prim = &prim;
|
||||
info.geom_type = MeshMaterialInfo::GeomType::Cube;
|
||||
|
||||
CollectBoundMaterial(abs_path, "", info.mesh_material);
|
||||
|
||||
collectorEnv->mesh_infos.push_back(std::move(info));
|
||||
}
|
||||
|
||||
// Process sphere geometry
|
||||
if (const tinyusdz::GeomSphere *psphere = prim.as<tinyusdz::GeomSphere>()) {
|
||||
(void)psphere; // Used for type check only
|
||||
MeshMaterialInfo info;
|
||||
info.abs_path = abs_path;
|
||||
info.prim = &prim;
|
||||
info.geom_type = MeshMaterialInfo::GeomType::Sphere;
|
||||
|
||||
CollectBoundMaterial(abs_path, "", info.mesh_material);
|
||||
|
||||
collectorEnv->mesh_infos.push_back(std::move(info));
|
||||
}
|
||||
|
||||
return true; // continue traversal
|
||||
}
|
||||
|
||||
// Task data for parallel mesh conversion
|
||||
struct MeshConversionTask {
|
||||
const MeshMaterialInfo *mesh_info{nullptr};
|
||||
RenderSceneConverter *converter{nullptr};
|
||||
const RenderSceneConverterEnv *env{nullptr};
|
||||
size_t output_index{0};
|
||||
RenderMesh *output_mesh{nullptr};
|
||||
std::string *output_error{nullptr};
|
||||
std::atomic<bool> *success{nullptr};
|
||||
};
|
||||
|
||||
// Worker function for parallel mesh conversion
|
||||
void MeshConversionWorker(void *user_data) {
|
||||
MeshConversionTask *task = static_cast<MeshConversionTask *>(user_data);
|
||||
if (!task || !task->mesh_info || !task->output_mesh) {
|
||||
return;
|
||||
}
|
||||
|
||||
const MeshMaterialInfo &info = *task->mesh_info;
|
||||
std::string local_err;
|
||||
|
||||
// Build MaterialPath from pre-collected info
|
||||
MaterialPath material_path;
|
||||
material_path.default_texcoords_primvar_name =
|
||||
task->env->mesh_config.default_texcoords_primvar_name;
|
||||
material_path.material_path = info.mesh_material.material_path;
|
||||
material_path.backface_material_path = info.mesh_material.backface_material_path;
|
||||
|
||||
// Build subset material path map
|
||||
std::map<std::string, MaterialPath> subset_material_path_map;
|
||||
for (const auto &subset_pair : info.subset_materials) {
|
||||
MaterialPath mpath;
|
||||
mpath.default_texcoords_primvar_name =
|
||||
task->env->mesh_config.default_texcoords_primvar_name;
|
||||
mpath.material_path = subset_pair.second.material_path;
|
||||
mpath.backface_material_path = subset_pair.second.backface_material_path;
|
||||
subset_material_path_map[subset_pair.first] = mpath;
|
||||
}
|
||||
|
||||
bool conversion_success = false;
|
||||
|
||||
if (info.geom_type == MeshMaterialInfo::GeomType::Mesh) {
|
||||
const tinyusdz::GeomMesh *pmesh = info.prim->as<tinyusdz::GeomMesh>();
|
||||
if (pmesh) {
|
||||
std::vector<const GeomSubset *> material_subsets =
|
||||
GetMaterialBindGeomSubsets(*info.prim);
|
||||
|
||||
std::vector<std::pair<std::string, const BlendShape *>> blendshapes;
|
||||
blendshapes = GetBlendShapes(task->env->stage, *info.prim, &local_err);
|
||||
|
||||
conversion_success = task->converter->ConvertMesh(
|
||||
*task->env, info.abs_path, *pmesh, material_path,
|
||||
subset_material_path_map, task->converter->materialMap,
|
||||
material_subsets, blendshapes, task->output_mesh);
|
||||
}
|
||||
} else if (info.geom_type == MeshMaterialInfo::GeomType::Cube) {
|
||||
const tinyusdz::GeomCube *pcube = info.prim->as<tinyusdz::GeomCube>();
|
||||
if (pcube) {
|
||||
std::vector<const tinyusdz::GeomSubset *> empty_subsets;
|
||||
std::vector<std::pair<std::string, const tinyusdz::BlendShape *>> empty_blendshapes;
|
||||
|
||||
conversion_success = task->converter->ConvertCube(
|
||||
*task->env, info.abs_path, *pcube, material_path,
|
||||
subset_material_path_map, task->converter->materialMap,
|
||||
empty_subsets, empty_blendshapes, task->output_mesh);
|
||||
}
|
||||
} else if (info.geom_type == MeshMaterialInfo::GeomType::Sphere) {
|
||||
const tinyusdz::GeomSphere *psphere = info.prim->as<tinyusdz::GeomSphere>();
|
||||
if (psphere) {
|
||||
std::vector<const tinyusdz::GeomSubset *> empty_subsets;
|
||||
std::vector<std::pair<std::string, const tinyusdz::BlendShape *>> empty_blendshapes;
|
||||
|
||||
conversion_success = task->converter->ConvertSphere(
|
||||
*task->env, info.abs_path, *psphere, material_path,
|
||||
subset_material_path_map, task->converter->materialMap,
|
||||
empty_subsets, empty_blendshapes, task->output_mesh);
|
||||
}
|
||||
}
|
||||
|
||||
if (!conversion_success) {
|
||||
task->success->store(false, std::memory_order_release);
|
||||
if (task->output_error) {
|
||||
*(task->output_error) = local_err.empty() ?
|
||||
"Mesh conversion failed" : local_err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // TINYUSDZ_ENABLE_THREAD
|
||||
|
||||
bool MeshVisitor(const tinyusdz::Path &abs_path, const tinyusdz::Prim &prim,
|
||||
const int32_t level, void *userdata, std::string *err) {
|
||||
if (!userdata) {
|
||||
@@ -9356,16 +9623,168 @@ bool RenderSceneConverter::ConvertToRenderScene(
|
||||
// 3. Convert Mesh/SkinWeights/BlendShapes
|
||||
// 4. Convert Skeleton(bones) and SkelAnimation
|
||||
//
|
||||
// Material conversion will be done in MeshVisitor.
|
||||
|
||||
#if defined(TINYUSDZ_ENABLE_THREAD)
|
||||
//
|
||||
MeshVisitorEnv menv;
|
||||
menv.env = &env;
|
||||
menv.converter = this;
|
||||
// Parallel mesh conversion path:
|
||||
// 1. Collect all materials (single-threaded to build material cache)
|
||||
// 2. Convert materials sequentially (populates materialMap)
|
||||
// 3. Convert meshes in parallel (materials already cached, read-only access)
|
||||
//
|
||||
if (env.scene_config.enable_parallel_mesh_conversion) {
|
||||
// Phase 1: Collect all materials and mesh info
|
||||
MaterialCollectorEnv collector_env;
|
||||
collector_env.env = &env;
|
||||
collector_env.converter = this;
|
||||
|
||||
bool ret = tydra::VisitPrims(env.stage, MeshVisitor, &menv, &err);
|
||||
bool collect_ret = tydra::VisitPrims(env.stage, MaterialCollectorVisitor,
|
||||
&collector_env, &err);
|
||||
if (!collect_ret) {
|
||||
PUSH_ERROR_AND_RETURN(err);
|
||||
}
|
||||
|
||||
if (!ret) {
|
||||
PUSH_ERROR_AND_RETURN(err);
|
||||
// Check if parallel conversion is worthwhile
|
||||
size_t num_meshes = collector_env.mesh_infos.size();
|
||||
bool use_parallel = num_meshes >= env.scene_config.min_meshes_for_parallel;
|
||||
|
||||
if (use_parallel) {
|
||||
// Phase 2: Convert all materials sequentially (builds material cache)
|
||||
for (const auto &mat_path : collector_env.material_paths) {
|
||||
if (materialMap.count(mat_path)) {
|
||||
continue; // Already converted
|
||||
}
|
||||
|
||||
auto mat_it = collector_env.material_ptr_map.find(mat_path);
|
||||
if (mat_it == collector_env.material_ptr_map.end() || !mat_it->second) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderMaterial rmat;
|
||||
Path mat_prim_path(mat_path, "");
|
||||
if (!ConvertMaterial(env, mat_prim_path, *(mat_it->second), &rmat)) {
|
||||
PUSH_ERROR_AND_RETURN(fmt::format("Material conversion failed: {}", mat_path));
|
||||
}
|
||||
|
||||
uint64_t mat_id = materials.size();
|
||||
materialMap.add(mat_path, mat_id);
|
||||
materials.push_back(std::move(rmat));
|
||||
}
|
||||
|
||||
// Report progress after material conversion (40%)
|
||||
if (!CallProgressCallback(0.4f)) {
|
||||
PushError("Conversion cancelled by user.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase 3: Convert meshes in parallel
|
||||
size_t num_threads = env.scene_config.num_mesh_conversion_threads;
|
||||
if (num_threads == 0) {
|
||||
num_threads = std::thread::hardware_concurrency();
|
||||
if (num_threads == 0) {
|
||||
num_threads = 4; // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-allocate output arrays
|
||||
std::vector<RenderMesh> output_meshes(num_meshes);
|
||||
std::vector<std::string> output_errors(num_meshes);
|
||||
std::vector<MeshConversionTask> tasks(num_meshes);
|
||||
std::atomic<bool> all_success(true);
|
||||
|
||||
// Initialize tasks
|
||||
for (size_t i = 0; i < num_meshes; i++) {
|
||||
tasks[i].mesh_info = &collector_env.mesh_infos[i];
|
||||
tasks[i].converter = this;
|
||||
tasks[i].env = &env;
|
||||
tasks[i].output_index = i;
|
||||
tasks[i].output_mesh = &output_meshes[i];
|
||||
tasks[i].output_error = &output_errors[i];
|
||||
tasks[i].success = &all_success;
|
||||
}
|
||||
|
||||
// Create task queue
|
||||
TaskQueue queue(env.scene_config.mesh_task_queue_capacity);
|
||||
std::atomic<bool> producer_done(false);
|
||||
|
||||
// Launch worker threads
|
||||
std::vector<std::thread> workers;
|
||||
workers.reserve(num_threads);
|
||||
|
||||
for (size_t t = 0; t < num_threads; t++) {
|
||||
workers.emplace_back([&queue, &producer_done]() {
|
||||
TaskItem task;
|
||||
while (!producer_done.load(std::memory_order_acquire) || !queue.Empty()) {
|
||||
if (queue.Pop(task)) {
|
||||
if (task.func) {
|
||||
task.func(task.user_data);
|
||||
}
|
||||
} else {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Producer: push all tasks
|
||||
for (size_t i = 0; i < tasks.size(); i++) {
|
||||
while (!queue.Push(MeshConversionWorker, &tasks[i])) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
|
||||
producer_done.store(true, std::memory_order_release);
|
||||
|
||||
// Wait for all workers to finish
|
||||
for (auto &worker : workers) {
|
||||
worker.join();
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (!all_success.load(std::memory_order_acquire)) {
|
||||
std::string combined_err;
|
||||
for (size_t i = 0; i < num_meshes; i++) {
|
||||
if (!output_errors[i].empty()) {
|
||||
combined_err += fmt::format("Mesh {} ({}): {}\n",
|
||||
i, collector_env.mesh_infos[i].abs_path.full_path_name(),
|
||||
output_errors[i]);
|
||||
}
|
||||
}
|
||||
PUSH_ERROR_AND_RETURN("Parallel mesh conversion failed:\n" + combined_err);
|
||||
}
|
||||
|
||||
// Move converted meshes to final storage and build meshMap
|
||||
meshes.reserve(meshes.size() + num_meshes);
|
||||
for (size_t i = 0; i < num_meshes; i++) {
|
||||
const std::string &path = collector_env.mesh_infos[i].abs_path.full_path_name();
|
||||
uint64_t mesh_id = meshes.size();
|
||||
meshMap.add(path, mesh_id);
|
||||
meshes.emplace_back(std::move(output_meshes[i]));
|
||||
}
|
||||
} else {
|
||||
// Fall back to sequential conversion for small scenes
|
||||
MeshVisitorEnv menv;
|
||||
menv.env = &env;
|
||||
menv.converter = this;
|
||||
|
||||
bool ret = tydra::VisitPrims(env.stage, MeshVisitor, &menv, &err);
|
||||
if (!ret) {
|
||||
PUSH_ERROR_AND_RETURN(err);
|
||||
}
|
||||
}
|
||||
} else
|
||||
#endif // TINYUSDZ_ENABLE_THREAD
|
||||
{
|
||||
// Sequential conversion path (original behavior)
|
||||
// Material conversion will be done in MeshVisitor.
|
||||
MeshVisitorEnv menv;
|
||||
menv.env = &env;
|
||||
menv.converter = this;
|
||||
|
||||
bool ret = tydra::VisitPrims(env.stage, MeshVisitor, &menv, &err);
|
||||
|
||||
if (!ret) {
|
||||
PUSH_ERROR_AND_RETURN(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Report progress after mesh/material conversion (70%)
|
||||
|
||||
@@ -2105,6 +2105,41 @@ struct RenderSceneConverterConfig {
|
||||
// Only effective when merge_meshes is true.
|
||||
//
|
||||
bool merge_meshes_bake_transform{true};
|
||||
|
||||
#if defined(TINYUSDZ_ENABLE_THREAD)
|
||||
//
|
||||
// Enable parallel mesh conversion for faster processing of scenes
|
||||
// with multiple meshes. When enabled:
|
||||
//
|
||||
// 1. Materials are pre-collected and converted first (single-threaded
|
||||
// to populate material cache)
|
||||
// 2. Meshes are then converted in parallel using worker threads
|
||||
//
|
||||
// This reduces conversion time for scenes with many objects by
|
||||
// utilizing multiple CPU cores for the expensive mesh conversion step.
|
||||
//
|
||||
// Only effective when TINYUSDZ_ENABLE_THREAD is defined.
|
||||
//
|
||||
bool enable_parallel_mesh_conversion{true};
|
||||
|
||||
//
|
||||
// Number of worker threads for parallel mesh conversion.
|
||||
// 0 = use hardware_concurrency() (number of logical cores)
|
||||
//
|
||||
size_t num_mesh_conversion_threads{0};
|
||||
|
||||
//
|
||||
// Minimum number of meshes required to trigger parallel conversion.
|
||||
// For small scenes, sequential conversion may be faster due to
|
||||
// thread creation overhead.
|
||||
//
|
||||
size_t min_meshes_for_parallel{4};
|
||||
|
||||
//
|
||||
// Task queue capacity for parallel mesh conversion.
|
||||
//
|
||||
size_t mesh_task_queue_capacity{1024};
|
||||
#endif // TINYUSDZ_ENABLE_THREAD
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user