From 1751733ddc2fc73928776865f07ccb1469aaa6e8 Mon Sep 17 00:00:00 2001 From: Eric Wasylishen Date: Mon, 1 Aug 2022 12:30:04 -0600 Subject: [PATCH 1/3] qbsp: add experimental MidSplit re-implementation, enabled by default --- include/common/aabb.hh | 5 ++ include/qbsp/qbsp.hh | 1 + qbsp/brushbsp.cc | 144 ++++++++--------------------------------- 3 files changed, 33 insertions(+), 117 deletions(-) diff --git a/include/common/aabb.hh b/include/common/aabb.hh index ee5a1245..996186e1 100644 --- a/include/common/aabb.hh +++ b/include/common/aabb.hh @@ -193,6 +193,11 @@ public: constexpr value_type centroid() const { return (m_mins + m_maxs) * 0.5; } + constexpr V volume() const { + auto s = size(); + return s[0] * s[1] * s[2]; + } + // stream support auto stream_data() { return std::tie(m_mins, m_maxs); } }; diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index eac457ef..d650352b 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -352,6 +352,7 @@ public: // since the max world size in Q3 is {-65536, -65536, -65536, 65536, 65536, 65536}. should we dynamically change this? // should we automatically turn this on if the world gets too big but leave it off for smaller worlds? setting_blocksize blocksize{this, "blocksize", { 0, 0, 0 }, &common_format_group, "from q3map2; split the world by x/y/z sized chunks, speeding up split decisions"}; + setting_int32 midsplitbrushes{this, "midsplitbrushes", 128, &debugging_group, "switch to cheaper partitioning if a node contains this many brushes"}; void setParameters(int argc, const char **argv) override { diff --git a/qbsp/brushbsp.cc b/qbsp/brushbsp.cc index dc9fe196..23beb9bc 100644 --- a/qbsp/brushbsp.cc +++ b/qbsp/brushbsp.cc @@ -631,26 +631,6 @@ static bool CheckPlaneAgainstVolume(const qbsp_plane_t &plane, node_t *node) return good; } - -/* - * Calculate the split plane metric for axial planes - */ -inline vec_t SplitPlaneMetric_Axial(const qbsp_plane_t &p, const aabb3d &bounds) -{ - vec_t value = 0; - for (int i = 0; i < 3; i++) { - if (static_cast(i) == p.get_type()) { - const vec_t dist = p.get_dist() * p.get_normal()[i]; - value += (bounds.maxs()[i] - dist) * (bounds.maxs()[i] - dist); - value += (dist - bounds.mins()[i]) * (dist - bounds.mins()[i]); - } else { - value += 2 * (bounds.maxs()[i] - bounds.mins()[i]) * (bounds.maxs()[i] - bounds.mins()[i]); - } - } - - return value; -} - /* * Split a bounding box by a plane; The front and back bounds returned * are such that they completely contain the portion of the input box @@ -710,31 +690,15 @@ inline void DivideBounds(const aabb3d &in_bounds, const qbsp_plane_t &split, aab } } -/* - * Calculate the split plane metric for non-axial planes - */ -inline vec_t SplitPlaneMetric_NonAxial(const qbsp_plane_t &p, const aabb3d &bounds) +inline vec_t SplitPlaneMetric(const qbsp_plane_t &p, const aabb3d &bounds) { aabb3d f, b; - vec_t value = 0.0; DivideBounds(bounds, p, f, b); - for (int i = 0; i < 3; i++) { - value += (f.maxs()[i] - f.mins()[i]) * (f.maxs()[i] - f.mins()[i]); - value += (b.maxs()[i] - b.mins()[i]) * (b.maxs()[i] - b.mins()[i]); - } - - return value; -} - -inline vec_t SplitPlaneMetric(const qbsp_plane_t &p, const aabb3d &bounds) -{ - if (p.get_type() < plane_type_t::PLANE_ANYX) { - return SplitPlaneMetric_Axial(p, bounds); - } else { - return SplitPlaneMetric_NonAxial(p, bounds); - } + // i.e. a good split will have equal volumes on front and back. + // a bad split will have all of the volume on one side. + return fabs(f.volume() - b.volume()); } /* @@ -744,78 +708,49 @@ ChooseMidPlaneFromList The clipping hull BSP doesn't worry about avoiding splits ================== */ -static std::optional ChooseMidPlaneFromList(const std::vector> &brushes, const aabb3d &bounds, bool forced) +static std::optional ChooseMidPlaneFromList(const std::vector> &brushes, const aabb3d &bounds) { - /* pick the plane that splits the least */ vec_t bestaxialmetric = VECT_MAX; std::optional bestaxialplane; + vec_t bestanymetric = VECT_MAX; std::optional bestanyplane; - for (int pass = 0; pass < 2; pass++) { - for (auto &brush : brushes) { - if ((pass & 1) && !brush->original->contents.is_any_detail(qbsp_options.target_game)) { - continue; + for (auto &brush : brushes) { + for (auto &side : brush->sides) { + if (side.bevel) { + continue; // never use a bevel as a spliter } - if (!(pass & 1) && brush->original->contents.is_any_detail(qbsp_options.target_game)) { - continue; + if (side.onnode) { + continue; // allready a node splitter } - for (auto &side : brush->sides) { - if (side.bevel) { - continue; // never use a bevel as a spliter - } - if (!side.w) { - continue; // nothing visible, so it can't split - } - if (side.onnode) { - continue; // allready a node splitter - } - if (side.get_texinfo().flags.is_hintskip) { - continue; // skip surfaces are never chosen - } + const qbsp_plane_t &plane = side.plane; + /* calculate the split metric, smaller values are better */ + const vec_t metric = SplitPlaneMetric(plane, bounds); - const qbsp_plane_t &plane = side.plane; - /* calculate the split metric, smaller values are better */ - const vec_t metric = SplitPlaneMetric(plane, bounds); + if (metric < bestanymetric) { + bestanymetric = metric; + bestanyplane = plane; + } - if (metric < bestanymetric) { - bestanymetric = metric; - bestanyplane = plane; - } - - /* check for axis aligned surfaces */ - if (plane.get_type() < plane_type_t::PLANE_ANYX) { - if (metric < bestaxialmetric) { - bestaxialmetric = metric; - bestaxialplane = plane; - } + /* check for axis aligned surfaces */ + if (plane.get_type() < plane_type_t::PLANE_ANYX) { + if (metric < bestaxialmetric) { + bestaxialmetric = metric; + bestaxialplane = plane; } } } - - if (bestanyplane || bestaxialplane) { - break; - } } // prefer the axial split - auto bestsurface = !bestaxialplane ? bestanyplane : bestaxialplane; + auto bestsurface = bestaxialplane ? bestaxialplane : bestanyplane; if (!bestsurface) { FError("No valid planes in surface list"); } - // ericw -- (!forced) is true on the final SolidBSP phase for the world. - // !bestsurface->has_struct means all surfaces in this node are detail, so - // mark the surface as a detail separator. - // fixme-brushbsp: what to do here? -#if 0 - if (!forced && !bestsurface->has_struct) { - bestsurface->detail_separator = true; - } -#endif - return bestsurface; } @@ -859,32 +794,8 @@ static std::optional SelectSplitPlane(const std::vector qbsp_options.midsplitsurffraction.value()); - } else { - // old way (ericw-tools 0.15.2+) - if (qbsp_options.maxnodesize.value() >= 64) { - const vec_t maxnodesize = qbsp_options.maxnodesize.value() - qbsp_options.epsilon.value(); - - largenode = (node->bounds.maxs()[0] - node->bounds.mins()[0]) > maxnodesize || - (node->bounds.maxs()[1] - node->bounds.mins()[1]) > maxnodesize || - (node->bounds.maxs()[2] - node->bounds.mins()[2]) > maxnodesize; - } - } - } - - // do fast way for clipping hull - if (use_mid_split || largenode) { - if (auto mid_plane = ChooseMidPlaneFromList(brushes, node->bounds, use_mid_split)) { + if (brushes.size() >= qbsp_options.midsplitbrushes.value()) { + if (auto mid_plane = ChooseMidPlaneFromList(brushes, node->bounds)) { for (auto &b : brushes) { b->side = TestBrushToPlanenum(*b, mid_plane.value(), nullptr, nullptr, nullptr); @@ -893,7 +804,6 @@ static std::optional SelectSplitPlane(const std::vector Date: Mon, 1 Aug 2022 15:51:57 -0400 Subject: [PATCH 2/3] adopt parent node bounds if a leaf is unbounded now too move midsplitbrushes into same group as blocksize always return a split rather than erroring list out which method is used of the three --- include/qbsp/qbsp.hh | 4 ++-- qbsp/brushbsp.cc | 19 +++++++++++-------- qbsp/portals.cc | 19 +++++++++---------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index d650352b..38d1dd82 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -284,7 +284,7 @@ public: setting_invertible_bool oldaxis{this, "oldaxis", true, &debugging_group, "uses alternate texture alignment which was default in tyrutils-ericw v0.15.1 and older"}; setting_bool forcegoodtree{ - this, "forcegoodtree", false, &debugging_group, "force use of expensive processing for SolidBSP stage"}; + this, "forcegoodtree", false, &debugging_group, "force use of expensive processing for BrushBSP stage"}; setting_scalar midsplitsurffraction{this, "midsplitsurffraction", 0.f, 0.f, 1.f, &debugging_group, "if 0 (default), use `maxnodesize` for deciding when to switch to midsplit bsp heuristic.\nif 0 < midsplitSurfFraction <= 1, switch to midsplit if the node contains more than this fraction of the model's\ntotal surfaces. Try 0.15 to 0.5. Works better than maxNodeSize for maps with a 3D skybox (e.g. +-128K unit maps)"}; setting_int32 maxnodesize{this, "maxnodesize", 1024, &debugging_group, @@ -352,7 +352,7 @@ public: // since the max world size in Q3 is {-65536, -65536, -65536, 65536, 65536, 65536}. should we dynamically change this? // should we automatically turn this on if the world gets too big but leave it off for smaller worlds? setting_blocksize blocksize{this, "blocksize", { 0, 0, 0 }, &common_format_group, "from q3map2; split the world by x/y/z sized chunks, speeding up split decisions"}; - setting_int32 midsplitbrushes{this, "midsplitbrushes", 128, &debugging_group, "switch to cheaper partitioning if a node contains this many brushes"}; + setting_int32 midsplitbrushes{this, "midsplitbrushes", 128, &common_format_group, "switch to cheaper partitioning if a node contains this many brushes"}; void setParameters(int argc, const char **argv) override { diff --git a/qbsp/brushbsp.cc b/qbsp/brushbsp.cc index 23beb9bc..7ed38470 100644 --- a/qbsp/brushbsp.cc +++ b/qbsp/brushbsp.cc @@ -54,8 +54,12 @@ struct bspstats_t std::atomic c_nodes; // number of nodes created by splitting on a side_t which had !visible std::atomic c_nonvis; + // total number of nodes created by qbsp3 method + std::atomic c_qbsp3; // total number of nodes created by block splitting std::atomic c_blocksplit; + // total number of nodes created by midsplit + std::atomic c_midsplit; // total number of leafs std::atomic c_leafs; }; @@ -745,13 +749,7 @@ static std::optional ChooseMidPlaneFromList(const std::vector SelectSplitPlane(const std::vector= qbsp_options.midsplitbrushes.value()) { + if (brushes.size() >= qbsp_options.midsplitbrushes.value() || use_mid_split) { if (auto mid_plane = ChooseMidPlaneFromList(brushes, node->bounds)) { + stats.c_midsplit++; for (auto &b : brushes) { b->side = TestBrushToPlanenum(*b, mid_plane.value(), nullptr, nullptr, nullptr); @@ -929,6 +928,8 @@ static std::optional SelectSplitPlane(const std::vectorplane; } @@ -1124,6 +1125,8 @@ static std::unique_ptr BrushBSP(mapentity_t *entity, std::vectorprint_content_stats(*stats.leafstats, "leafs"); diff --git a/qbsp/portals.cc b/qbsp/portals.cc index 8af1cd5f..35ceafdb 100644 --- a/qbsp/portals.cc +++ b/qbsp/portals.cc @@ -389,18 +389,17 @@ static void CalcTreeBounds_r(tree_t *tree, node_t *node) { if (node->is_leaf) { CalcNodeBounds(node); - return; + } else { + tbb::task_group g; + g.run([&]() { CalcTreeBounds_r(tree, node->children[0].get()); }); + g.run([&]() { CalcTreeBounds_r(tree, node->children[1].get()); }); + g.wait(); + + node->bounds = node->children[0]->bounds + node->children[1]->bounds; } - tbb::task_group g; - g.run([&]() { CalcTreeBounds_r(tree, node->children[0].get()); }); - g.run([&]() { CalcTreeBounds_r(tree, node->children[1].get()); }); - g.wait(); - - node->bounds = node->children[0]->bounds + node->children[1]->bounds; - if (node->bounds.mins()[0] >= node->bounds.maxs()[0]) { - logging::print("WARNING: node without a volume\n"); + logging::print("WARNING: {} without a volume\n", node->is_leaf ? "leaf" : "node"); // fixme-brushbsp: added this to work around leafs with no portals showing up in "qbspfeatures.map" among other // test maps. Not sure if correct or there's another underlying problem. @@ -409,7 +408,7 @@ static void CalcTreeBounds_r(tree_t *tree, node_t *node) for (auto &v : node->bounds.mins()) { if (fabs(v) > qbsp_options.worldextent.value()) { - logging::print("WARNING: node with unbounded volume\n"); + logging::print("WARNING: {} with unbounded volume\n", node->is_leaf ? "leaf" : "node"); break; } } From 8476e2861c67bc2d48ebb2a5d83cd74e430d3925 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 2 Aug 2022 04:29:36 -0400 Subject: [PATCH 3/3] this all matches release now --- include/qbsp/brushbsp.hh | 2 +- include/qbsp/qbsp.hh | 2 +- qbsp/brushbsp.cc | 110 ++++++++++++++++++++++++++------------- qbsp/qbsp.cc | 2 +- 4 files changed, 76 insertions(+), 40 deletions(-) diff --git a/include/qbsp/brushbsp.hh b/include/qbsp/brushbsp.hh index 71551324..6b84e392 100644 --- a/include/qbsp/brushbsp.hh +++ b/include/qbsp/brushbsp.hh @@ -41,4 +41,4 @@ bool WindingIsTiny(const winding_t &w, double size = EDGE_LENGTH_EPSILON); std::unique_ptr BrushFromBounds(const aabb3d &bounds); // compatibility version -std::unique_ptr BrushBSP(mapentity_t *entity, bool use_mid_split); +std::unique_ptr BrushBSP(mapentity_t *entity, std::optional forced_quick_tree); diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index 38d1dd82..0e5db61a 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -352,7 +352,7 @@ public: // since the max world size in Q3 is {-65536, -65536, -65536, 65536, 65536, 65536}. should we dynamically change this? // should we automatically turn this on if the world gets too big but leave it off for smaller worlds? setting_blocksize blocksize{this, "blocksize", { 0, 0, 0 }, &common_format_group, "from q3map2; split the world by x/y/z sized chunks, speeding up split decisions"}; - setting_int32 midsplitbrushes{this, "midsplitbrushes", 128, &common_format_group, "switch to cheaper partitioning if a node contains this many brushes"}; + setting_numeric midsplitbrushfraction{this, "midsplitbrushfraction", 0.0, &common_format_group, "switch to cheaper partitioning if a node contains this % of brushes in the map"}; void setParameters(int argc, const char **argv) override { diff --git a/qbsp/brushbsp.cc b/qbsp/brushbsp.cc index 7ed38470..2b6dead5 100644 --- a/qbsp/brushbsp.cc +++ b/qbsp/brushbsp.cc @@ -626,7 +626,7 @@ static void CheckPlaneAgainstParents(const qbsp_plane_t &plane, node_t *node) } } -static bool CheckPlaneAgainstVolume(const qbsp_plane_t &plane, node_t *node) +static bool CheckPlaneAgainstVolume(const qbsp_plane_t &plane, const node_t *node) { auto [front, back] = SplitBrush(node->volume->copy_unique(), plane); @@ -712,7 +712,7 @@ ChooseMidPlaneFromList The clipping hull BSP doesn't worry about avoiding splits ================== */ -static std::optional ChooseMidPlaneFromList(const std::vector> &brushes, const aabb3d &bounds) +static std::optional ChooseMidPlaneFromList(const std::vector> &brushes, const node_t *node) { vec_t bestaxialmetric = VECT_MAX; std::optional bestaxialplane; @@ -730,8 +730,13 @@ static std::optional ChooseMidPlaneFromList(const std::vectorbounds); if (metric < bestanymetric) { bestanymetric = metric; @@ -761,46 +766,77 @@ Using heuristics, chooses a plane to partition the brushes with. Returns nullopt if there are no valid planes to split with. ================ */ -static std::optional SelectSplitPlane(const std::vector> &brushes, node_t *node, bool use_mid_split, bspstats_t &stats) +static std::optional SelectSplitPlane(const std::vector> &brushes, node_t *node, std::optional forced_quick_tree, bspstats_t &stats) { // no brushes left to split, so we can't use any plane. if (!brushes.size()) { return std::nullopt; } - // if it is crossing a block boundary, force a split; - // this is optional q3map2 mode - for (size_t i = 0; i < 3; i++) { - if (qbsp_options.blocksize.value()[i] <= 0) { - continue; - } + // if forced_quick_tree is nullopt, we will choose fast/slow based on + // certain parameters. + if (!forced_quick_tree.has_value() || forced_quick_tree.value() == true) { + // if it is crossing a block boundary, force a split; + // this is optional q3map2 mode that is disabled by default. + if (qbsp_options.blocksize.isChanged()) { + for (size_t i = 0; i < 3; i++) { + if (qbsp_options.blocksize.value()[i] <= 0) { + continue; + } - vec_t dist = qbsp_options.blocksize.value()[i] * (floor(node->bounds.mins()[i] / qbsp_options.blocksize.value()[i]) + 1); + vec_t dist = qbsp_options.blocksize.value()[i] * (floor(node->bounds.mins()[i] / qbsp_options.blocksize.value()[i]) + 1); - if (node->bounds.maxs()[i] > dist) { - qplane3d plane{}; - plane.normal[i] = 1.0; - plane.dist = dist; - qbsp_plane_t bsp_plane = plane; - stats.c_blocksplit++; + if (node->bounds.maxs()[i] > dist) { + qplane3d plane{}; + plane.normal[i] = 1.0; + plane.dist = dist; + qbsp_plane_t bsp_plane = plane; - for (auto &b : brushes) { - b->side = TestBrushToPlanenum(*b, bsp_plane, nullptr, nullptr, nullptr); + if (!CheckPlaneAgainstVolume(bsp_plane, node)) { + continue; // would produce a tiny volume + } + + stats.c_blocksplit++; + + for (auto &b : brushes) { + b->side = TestBrushToPlanenum(*b, bsp_plane, nullptr, nullptr, nullptr); + } + + return bsp_plane; + } + } + } + + if (!forced_quick_tree.has_value()) { + + // decide if we should switch to the midsplit method + if (qbsp_options.midsplitbrushfraction.value() != 0.0) { + // new way (opt-in) + // how much of the map are we partitioning? + double fractionOfMap = brushes.size() / (double) map.brushes.size(); + forced_quick_tree = (fractionOfMap > qbsp_options.midsplitbrushfraction.value()); + } else { + // old way (ericw-tools 0.15.2+) + if (qbsp_options.maxnodesize.value() >= 64) { + const vec_t maxnodesize = qbsp_options.maxnodesize.value() - qbsp_options.epsilon.value(); + + forced_quick_tree = (node->bounds.maxs()[0] - node->bounds.mins()[0]) > maxnodesize + || (node->bounds.maxs()[1] - node->bounds.mins()[1]) > maxnodesize + || (node->bounds.maxs()[2] - node->bounds.mins()[2]) > maxnodesize; + } } + } - return bsp_plane; - } - } + if (forced_quick_tree.value()) { + if (auto mid_plane = ChooseMidPlaneFromList(brushes, node)) { + stats.c_midsplit++; - if (brushes.size() >= qbsp_options.midsplitbrushes.value() || use_mid_split) { - if (auto mid_plane = ChooseMidPlaneFromList(brushes, node->bounds)) { - stats.c_midsplit++; + for (auto &b : brushes) { + b->side = TestBrushToPlanenum(*b, mid_plane.value(), nullptr, nullptr, nullptr); + } - for (auto &b : brushes) { - b->side = TestBrushToPlanenum(*b, mid_plane.value(), nullptr, nullptr, nullptr); + return mid_plane; } - - return mid_plane; } } @@ -991,10 +1027,10 @@ BuildTree_r Called in parallel. ================== */ -static void BuildTree_r(node_t *node, std::vector> brushes, bool use_mid_split, bspstats_t &stats) +static void BuildTree_r(node_t *node, std::vector> brushes, std::optional forced_quick_tree, bspstats_t &stats) { // find the best plane to use as a splitter - auto bestplane = SelectSplitPlane(brushes, node, use_mid_split, stats); + auto bestplane = SelectSplitPlane(brushes, node, forced_quick_tree, stats); if (!bestplane) { // this is a leaf node @@ -1034,8 +1070,8 @@ static void BuildTree_r(node_t *node, std::vector> b // recursively process children tbb::task_group g; - g.run([&]() { BuildTree_r(node->children[0].get(), std::move(children[0]), use_mid_split, stats); }); - g.run([&]() { BuildTree_r(node->children[1].get(), std::move(children[1]), use_mid_split, stats); }); + g.run([&]() { BuildTree_r(node->children[0].get(), std::move(children[0]), forced_quick_tree, stats); }); + g.run([&]() { BuildTree_r(node->children[1].get(), std::move(children[1]), forced_quick_tree, stats); }); g.wait(); } @@ -1044,7 +1080,7 @@ static void BuildTree_r(node_t *node, std::vector> b BrushBSP ================== */ -static std::unique_ptr BrushBSP(mapentity_t *entity, std::vector> brushlist, bool use_mid_split) +static std::unique_ptr BrushBSP(mapentity_t *entity, std::vector> brushlist, std::optional forced_quick_tree) { auto tree = std::make_unique(); @@ -1120,7 +1156,7 @@ static std::unique_ptr BrushBSP(mapentity_t *entity, std::vectorcreate_content_stats(); - BuildTree_r(tree->headnode.get(), std::move(brushlist), use_mid_split, stats); + BuildTree_r(tree->headnode.get(), std::move(brushlist), forced_quick_tree, stats); logging::print(logging::flag::STAT, " {:8} visible nodes\n", stats.c_nodes - stats.c_nonvis); logging::print(logging::flag::STAT, " {:8} nonvis nodes\n", stats.c_nonvis); @@ -1133,7 +1169,7 @@ static std::unique_ptr BrushBSP(mapentity_t *entity, std::vector BrushBSP(mapentity_t *entity, bool use_mid_split) +std::unique_ptr BrushBSP(mapentity_t *entity, std::optional forced_quick_tree) { - return BrushBSP(entity, MakeBspBrushList(entity), use_mid_split); + return BrushBSP(entity, MakeBspBrushList(entity), forced_quick_tree); } diff --git a/qbsp/qbsp.cc b/qbsp/qbsp.cc index b8bd7607..efbed9f3 100644 --- a/qbsp/qbsp.cc +++ b/qbsp/qbsp.cc @@ -609,7 +609,7 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum) if (qbsp_options.forcegoodtree.value()) { tree = BrushBSP(entity, false); } else { - tree = BrushBSP(entity, entity == map.world_entity()); + tree = BrushBSP(entity, entity == map.world_entity() ? std::nullopt : std::optional(false)); } // build all the portals in the bsp tree