diff --git a/src/libslic3r/MacUtils.hpp b/src/libslic3r/MacUtils.hpp index 388baa11902..802cc1e4225 100644 --- a/src/libslic3r/MacUtils.hpp +++ b/src/libslic3r/MacUtils.hpp @@ -4,6 +4,7 @@ namespace Slic3r { bool is_macos_support_boost_add_file_log(); +bool IsMacVersion15(); } diff --git a/src/libslic3r/MacUtils.mm b/src/libslic3r/MacUtils.mm index bb9ecd1e463..93a1e211ef9 100644 --- a/src/libslic3r/MacUtils.mm +++ b/src/libslic3r/MacUtils.mm @@ -12,4 +12,16 @@ bool is_macos_support_boost_add_file_log() } } +bool IsMacVersion15() +{ + if (@available(macOS 15.0, *)) + { + return true; + } + else + { + return false; + } +} + }; // namespace Slic3r diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 95dfc829ebc..2863f6f5aa9 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -569,6 +569,8 @@ set(SLIC3R_GUI_SOURCES Utils/bambu_networking.hpp Utils/Bonjour.cpp Utils/Bonjour.hpp + Utils/CpuMemory.cpp + Utils/CpuMemory.hpp Utils/CalibUtils.cpp Utils/CalibUtils.hpp Utils/ColorSpaceConvert.cpp diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 234287414ba..7c6cbae2e8c 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -22,6 +22,9 @@ #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Tesselate.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/QuadricEdgeCollapse.hpp" +#include +#include #include #include @@ -104,6 +107,97 @@ Slic3r::ColorRGBA adjust_color_for_rendering(const Slic3r::ColorRGBA& colors) namespace Slic3r { +// LOD mesh sharing map: maps TriangleMesh* -> LOD entry for that mesh. +// When multiple volumes reference the same TriangleMesh, LOD simplified models are shared. +// The entry holds an owning shared_ptr to the mesh, so the raw pointer used as +// lookup key cannot dangle or be reused by another mesh while the entry +// exists. Entries are maintained by load_object_volume()/release_volume(): +// a volume registers itself on creation and is removed on deletion; the entry +// dies (releasing the mesh) with its last volume. +struct MeshLodEntry { + std::shared_ptr mesh; // keeps the key mesh alive + std::set volumes; +}; +static std::map g_meshVolumesMap; + +// LOD run-time constants +const unsigned char LOD_UPDATE_FREQUENCY = 20; +const float ZOOM_THRESHOLD = 0.3f; +// pixel thresholds for LOD screen-size evaluation +const Vec2i32 LOD_SCREEN_MIN = Vec2i32(150, 110); +const Vec2i32 LOD_SCREEN_MAX = Vec2i32(300, 200); +const int SUPER_LARGE_FACES = 500000; +const int LARGE_FACES = 100000; + +//QEM face threshold +const int INIT_FACE_LOW_COUNT = 200; +const int FINAL_FACE_LOW_COUNT = 1000; +const float QEM_FACE_RATIO = 0.5f; +const float AABB_RANGE_EPSILON = 1.0f; + +//QEM Middle Small max error threshold +const float MIDDLE_LOD_NORMAL_FACE_MAX_ERROR = 0.1f; +const float MIDDLE_LOD_SUPER_LARGE_FACE_MAX_ERROR = 0.08f; +const float MIDDLE_LOD_LARGE_FACE_MAX_ERROR = 0.05f; + +const float SMALL_LOD_NORMAL_FACE_MAX_ERROR = 0.5f; +const float SMALL_LOD_SUPER_LARGE_FACE_MAX_ERROR = 0.4f; +const float SMALL_LOD_LARGE_FACE_MAX_ERROR = 0.3f; + +// Cached camera state for LOD evaluation +float GLVolume::s_lastCameraZoomValue = 0.0f; +float GLVolume::s_curZoom = 1.0f; +Matrix4d GLVolume::s_curViewProjMatrix = Matrix4d::Identity(); +std::array GLVolume::s_curViewport = {0, 0, 0, 0}; + +// Project a 3D point to 2D screen coordinates using the view-projection matrix +static Vec2f CalcPtInScreen(const Vec3d& pt, const Matrix4d& viewProjMat, int windowWidth, int windowHeight) +{ + Vec4d point(pt.x(), pt.y(), pt.z(), 1.0); + Vec4d pointNDCSpace = viewProjMat * point; + Vec3d pointScreenSpace = Vec3d(pointNDCSpace.x(), pointNDCSpace.y(), pointNDCSpace.z()) / pointNDCSpace.w(); + float x = 0.5f * (1 + pointScreenSpace(0)) * windowWidth; + float y = 0.5f * (1 - pointScreenSpace(1)) * windowHeight; + return Vec2f(x, y); +} + +// Determine which LOD level to use based on the volume's bounding box screen-space size +static LODLevel CalcVolumeBoxInScreenBiggerThanThreshold(const BoundingBoxf3& worldAABB, const Matrix4d& viewProjMat, int windowWidth, int windowHeight) +{ + const Vec3d& min3d = worldAABB.min; + const Vec3d& max3d = worldAABB.max; + std::array srcVertices; + srcVertices[0] = min3d; + srcVertices[1] = Vec3d(max3d.x(), min3d.y(), min3d.z()); + srcVertices[2] = Vec3d(max3d.x(), max3d.y(), min3d.z()); + srcVertices[3] = Vec3d(min3d.x(), max3d.y(), min3d.z()); + srcVertices[4] = Vec3d(min3d.x(), min3d.y(), max3d.z()); + srcVertices[5] = Vec3d(max3d.x(), min3d.y(), max3d.z()); + srcVertices[6] = max3d; + srcVertices[7] = Vec3d(min3d.x(), max3d.y(), max3d.z()); + + BoundingBoxf box2d; + for (int i = 0; i < srcVertices.size(); i++) + { + Vec2f screenPt = CalcPtInScreen(srcVertices[i], viewProjMat, windowWidth, windowHeight); + box2d.merge(screenPt.cast()); + } + double sizeX = box2d.size().x(); + double sizeY = box2d.size().y(); + if (sizeX >= LOD_SCREEN_MAX.x() || sizeY >= LOD_SCREEN_MAX.y()) + { + return LODLevel::High; + } + if (sizeX <= LOD_SCREEN_MIN.x() && sizeY <= LOD_SCREEN_MIN.y()) + { + return LODLevel::Small; + } + else + { + return LODLevel::Middle; + } +} + const float GLVolume::SinkingContours::HalfWidth = 0.25f; void GLVolume::SinkingContours::render() @@ -223,6 +317,7 @@ GLVolume::GLVolume(float r, float g, float b, float a) , force_sinking_contours(false) , picking(false) , tverts_range(0, size_t(-1)) + , m_tvertsRangeLod(0, size_t(-1)) { color = {r, g, b, a}; set_render_color(color); @@ -310,6 +405,130 @@ ColorRGBA color_from_model_volume(const ModelVolume& model_volume) return color; } +bool GLVolume::SimplifyMesh(const TriangleMesh& mesh, std::shared_ptr model, std::shared_ptr> readyFlag, LODLevel lod) const +{ + return SimplifyMesh(mesh.its, model, readyFlag, lod); +} + +bool GLVolume::SimplifyMesh(const indexed_triangle_set& its, std::shared_ptr model, std::shared_ptr> readyFlag, LODLevel lod) const +{ + if (its.indices.size() == 0 || its.vertices.size() == 0) + { + return false; + } + + auto itsCopy = std::make_unique(its); + + float maxError = std::numeric_limits::max(); + if (lod == LODLevel::Middle) + { + maxError = MIDDLE_LOD_NORMAL_FACE_MAX_ERROR; + if (its.indices.size() > SUPER_LARGE_FACES) + { + maxError = MIDDLE_LOD_SUPER_LARGE_FACE_MAX_ERROR; + } + else if(its.indices.size() > LARGE_FACES) + { + maxError = MIDDLE_LOD_LARGE_FACE_MAX_ERROR; + } + } + if (lod == LODLevel::Small) + { + maxError = SMALL_LOD_NORMAL_FACE_MAX_ERROR; + if (its.indices.size() > SUPER_LARGE_FACES) + { + maxError = SMALL_LOD_SUPER_LARGE_FACE_MAX_ERROR; + } + else if(its.indices.size() > LARGE_FACES) + { + maxError = SMALL_LOD_LARGE_FACE_MAX_ERROR; + } + } + + TriangleMesh originMesh(*itsCopy); + + // Run simplification in background thread (async, detached) + // Ref: https://people.eecs.berkeley.edu/~jrs/meshpapers/GarlandHeckbert2.pdf + std::thread worker = std::thread( + [model, readyFlag, maxError, originMesh](std::unique_ptr itsPtr) { + int initFaceCount = itsPtr->indices.size(); + uint32_t triangleCount = 0; + float maxErrCopy = maxError; + + its_quadric_edge_collapse(*itsPtr, triangleCount, &maxErrCopy); + + // Validate simplification quality + int endFaceCount = (*itsPtr).indices.size(); + if (initFaceCount < INIT_FACE_LOW_COUNT || (initFaceCount < FINAL_FACE_LOW_COUNT && endFaceCount < initFaceCount * QEM_FACE_RATIO)) + { + BOOST_LOG_TRIVIAL(info) << "LOD simplify: rejected (too few faces) init=" << initFaceCount << " end=" << endFaceCount; + return; + } + + TriangleMesh simplifiedMesh(*itsPtr); + Vec3f originMin = originMesh.stats().min - Vec3f(AABB_RANGE_EPSILON, AABB_RANGE_EPSILON, AABB_RANGE_EPSILON); + Vec3f originMax = originMesh.stats().max + Vec3f(AABB_RANGE_EPSILON, AABB_RANGE_EPSILON, AABB_RANGE_EPSILON); + + // Ensure simplified mesh stays within original bounding box + if (originMin.x() < simplifiedMesh.stats().min.x() && + originMin.y() < simplifiedMesh.stats().min.y() && + originMin.z() < simplifiedMesh.stats().min.z() && + originMax.x() > simplifiedMesh.stats().max.x() && + originMax.y() > simplifiedMesh.stats().max.y() && + originMax.z() > simplifiedMesh.stats().max.z()) { + if (model && model.use_count() >= 2) { + // The model is render-disabled until the main thread sees + // readyFlag (GLModel.hpp threading contract), so this + // write is exclusive to this thread. + model->init_from(simplifiedMesh); + BOOST_LOG_TRIVIAL(info) << "LOD simplify: completed successfully, faces=" << initFaceCount + << " -> " << endFaceCount + << " (use_count=" << model.use_count() << ")"; + } else { + BOOST_LOG_TRIVIAL(info) << "LOD simplify: skipped init (use_count=" + << (model ? model.use_count() : 0) << ")"; + } + } else { + BOOST_LOG_TRIVIAL(info) << "LOD simplify: rejected (out of AABB bounds)"; + } + + // Last touch of the model: hand it over to the main thread. The + // release store pairs with the acquire load in + // promote_ready_lod_models(), making the init_from() writes above + // visible before enable_render() is called. + if (readyFlag) + readyFlag->store(true, std::memory_order_release); + }, + std::move(itsCopy)); + + if (worker.joinable()) + { + worker.detach(); + } + return true; +} + +void GLVolume::set_bounding_boxes_as_dirty() +{ + // Force immediate LOD re-evaluation + m_lodUpdateIndex = LOD_UPDATE_FREQUENCY; + m_transformed_bounding_box.reset(); + m_transformed_convex_hull_bounding_box.reset(); + m_transformed_non_sinking_bounding_box.reset(); +} + +void GLVolume::promote_ready_lod_models() +{ + // The LOD models stay render-disabled while their background thread may + // still be writing them. Once the worker signals completion (release + // store in SimplifyMesh), the acquire load below makes its writes + // visible, and enable_render() hands the model over to the main thread. + if (m_modelMiddle && m_lodMiddleReady && m_modelMiddle->is_render_disabled() && m_lodMiddleReady->load(std::memory_order_acquire)) + m_modelMiddle->enable_render(); + if (m_modelSmall && m_lodSmallReady && m_modelSmall->is_render_disabled() && m_lodSmallReady->load(std::memory_order_acquire)) + m_modelSmall->enable_render(); +} + Transform3d GLVolume::world_matrix() const { Transform3d m = m_instance_transformation.get_matrix() * m_volume_transformation.get_matrix(); @@ -429,6 +648,114 @@ void GLVolume::render() simple_render(shader, model_objects, colors); } +// BBS: add outline related logic +void GLVolume::render_with_outline(const GUI::Size& cnv_size) +{ + if (!is_active) + return; + + GLShaderProgram* shader = GUI::wxGetApp().get_current_shader(); + if (shader == nullptr) + return; + + ModelObjectPtrs& model_objects = GUI::wxGetApp().model().objects; + std::vector colors = get_extruders_colors(); + + const GUI::OpenGLManager::EFramebufferType framebuffers_type = GUI::OpenGLManager::get_framebuffers_type(); + if (framebuffers_type == GUI::OpenGLManager::EFramebufferType::Unknown) { + // No supported, degrade to normal rendering + simple_render(shader, model_objects, colors); + return; + } + + // 1st. render pass, render the model into a separate render target that has only depth buffer + GLuint depth_fbo = 0; + GLuint depth_tex = 0; + if (framebuffers_type == GUI::OpenGLManager::EFramebufferType::Arb) { + glsafe(::glGenFramebuffers(1, &depth_fbo)); + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, depth_fbo)); + + glActiveTexture(GL_TEXTURE0); + glsafe(::glGenTextures(1, &depth_tex)); + glsafe(::glBindTexture(GL_TEXTURE_2D, depth_tex)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, cnv_size.get_width(), cnv_size.get_height(), 0, GL_DEPTH_COMPONENT, + GL_FLOAT, nullptr)); + + glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_tex, 0)); + } else { + glsafe(::glGenFramebuffersEXT(1, &depth_fbo)); + glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, depth_fbo)); + + glActiveTexture(GL_TEXTURE0); + glsafe(::glGenTextures(1, &depth_tex)); + glsafe(::glBindTexture(GL_TEXTURE_2D, depth_tex)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, cnv_size.get_width(), cnv_size.get_height(), 0, GL_DEPTH_COMPONENT, + GL_FLOAT, nullptr)); + + glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, depth_tex, 0)); + } + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + { + // Use LOD model for depth pass if available (consistent with body rendering) + auto renderDepthModel = [this]() { + if (!picking) { + if (m_curLodLevel == LODLevel::Small && m_modelSmall && !m_modelSmall->is_render_disabled() && m_modelSmall->is_initialized()) { + m_modelSmall->set_color(render_color); + m_modelSmall->render(); + return; + } + if (m_curLodLevel == LODLevel::Middle && m_modelMiddle && !m_modelMiddle->is_render_disabled() && m_modelMiddle->is_initialized()) { + m_modelMiddle->set_color(render_color); + m_modelMiddle->render(); + return; + } + } + model.set_color(render_color); + if (tverts_range == std::make_pair(0, -1)) + model.render(); + else + model.render(this->tverts_range); + }; + renderDepthModel(); + } + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + + // 2nd. render pass, just a normal render with the depth buffer passed as a texture + if (framebuffers_type == GUI::OpenGLManager::EFramebufferType::Arb) { + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, 0)); + } else if (framebuffers_type == GUI::OpenGLManager::EFramebufferType::Ext) { + glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)); + } + shader->set_uniform("is_outline", true); + shader->set_uniform("screen_size", Vec2f{cnv_size.get_width(), cnv_size.get_height()}); + glActiveTexture(GL_TEXTURE0); + glsafe(::glBindTexture(GL_TEXTURE_2D, depth_tex)); + shader->set_uniform("depth_tex", 0); + simple_render(shader, model_objects, colors); + + // Some clean up to do + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + shader->set_uniform("is_outline", false); + if (framebuffers_type == GUI::OpenGLManager::EFramebufferType::Arb) { + glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, 0)); + if (depth_fbo != 0) + glsafe(::glDeleteFramebuffers(1, &depth_fbo)); + } else if (framebuffers_type == GUI::OpenGLManager::EFramebufferType::Ext) { + glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)); + if (depth_fbo != 0) + glsafe(::glDeleteFramebuffersEXT(1, &depth_fbo)); + } + if (depth_tex != 0) + glsafe(::glDeleteTextures(1, &depth_tex)); +} // BBS add render for simple case void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_objects, @@ -467,6 +794,9 @@ void GLVolume::simple_render(GLShaderProgram* shader, } } while (0); + // LOD evaluation is now done once per frame in GLVolumeCollection::render(). + // m_curLodLevel is already set before simple_render is called. + if (color_volume && !picking) { // when force_transparent, we need to keep the alpha if (force_native_color && render_color.is_transparent()) { @@ -517,10 +847,45 @@ void GLVolume::simple_render(GLShaderProgram* shader, m.render(this->tverts_range); } } else { - if (tverts_range == std::make_pair(0, -1)) - model.render(); - else - model.render(this->tverts_range); + // Select LOD model based on current LOD level + static int lodRenderLogCounter = 0; + lodRenderLogCounter++; + if (!picking) { + // DEBUG: color-code LOD levels for visual verification + // GREEN = HIGH (original), BLUE = MIDDLE, RED = SMALL + if (m_curLodLevel == LODLevel::Small && m_modelSmall && !m_modelSmall->is_render_disabled() && m_modelSmall->is_initialized()) { + if (lodRenderLogCounter % 180 == 0) + BOOST_LOG_TRIVIAL(debug) << "LOD: SMALL '" << name << "'"; + m_modelSmall->set_color(render_color); + //m_modelSmall->set_color(ColorRGBA::GREEN()); + m_modelSmall->render(); + } else if (m_curLodLevel == LODLevel::Middle && m_modelMiddle && !m_modelMiddle->is_render_disabled() && m_modelMiddle->is_initialized()) { + if (lodRenderLogCounter % 180 == 0) + BOOST_LOG_TRIVIAL(debug) << "LOD: MID '" << name << "'"; + m_modelMiddle->set_color(render_color); + //m_modelMiddle->set_color(ColorRGBA::BLUE()); + m_modelMiddle->render(); + } else { + if (lodRenderLogCounter % 180 == 0) { + BOOST_LOG_TRIVIAL(debug) << "LOD: HIGH fallback '" << name + << "' lv=" << static_cast(m_curLodLevel) + << " s=" << (m_modelSmall ? (int)(!m_modelSmall->is_render_disabled() && m_modelSmall->is_initialized()) : -1) + << " m=" << (m_modelMiddle ? (int)(!m_modelMiddle->is_render_disabled() && m_modelMiddle->is_initialized()) : -1); + } + // model.set_color() already called in render loop line 1301 + //model.set_color(ColorRGBA::RED()); + if (tverts_range == std::make_pair(0, -1)) + model.render(); + else + model.render(this->tverts_range); + } + } else { + // Picking: always use full-resolution model + if (tverts_range == std::make_pair(0, -1)) + model.render(); + else + model.render(this->tverts_range); + } } if (this->is_left_handed()) glFrontFace(GL_CCW); @@ -584,13 +949,15 @@ std::vector GLVolumeCollection::load_object(const ModelObject* model_o const std::vector& instance_idxs, const std::string& color_by, bool opengl_initialized, - bool need_raycaster) + bool need_raycaster, + bool lodEnabled) { std::vector volumes_idx; for (int volume_idx = 0; volume_idx < int(model_object->volumes.size()); ++volume_idx) for (int instance_idx : instance_idxs) - volumes_idx.emplace_back(this->GLVolumeCollection::load_object_volume(model_object, obj_idx, volume_idx, instance_idx, color_by, - opengl_initialized, false, false, need_raycaster)); + volumes_idx.emplace_back(this->GLVolumeCollection::load_object_volume( + model_object, obj_idx, volume_idx, instance_idx, color_by, + opengl_initialized, false, false, need_raycaster, lodEnabled)); return volumes_idx; } @@ -602,7 +969,8 @@ int GLVolumeCollection::load_object_volume(const ModelObject* model_object, bool opengl_initialized, bool in_assemble_view, bool use_loaded_id, - bool need_raycaster) + bool need_raycaster, + bool lodEnabled) { const ModelVolume* model_volume = model_object->volumes[volume_idx]; const int extruder_id = model_volume->extruder_id(); @@ -610,20 +978,71 @@ int GLVolumeCollection::load_object_volume(const ModelObject* model_object, auto color = GLVolume::MODEL_COLOR[((color_by == "volume") ? volume_idx : obj_idx) % 4]; color.a(model_volume->is_model_part() ? 0.7f : 0.4f); - std::shared_ptr mesh = model_volume->mesh_ptr(); + std::shared_ptr meshSharedPtr = model_volume->mesh_ptr(); + const TriangleMesh* meshPtr = meshSharedPtr.get(); this->volumes.emplace_back(new GLVolume(color)); GLVolume& v = *this->volumes.back(); v.set_color(color_from_model_volume(*model_volume)); v.name = model_volume->name; + // LOD mesh sharing: if another volume already loaded this mesh, reuse its LOD data + v.m_oriMesh = meshPtr; + auto iter = g_meshVolumesMap.find(meshPtr); + if (iter != g_meshVolumesMap.end()) { + MeshLodEntry& entry = iter->second; + if (!entry.volumes.empty()) { + GLVolume* firstVolume = *entry.volumes.begin(); + // Share LOD models via shared_ptr (ref-counted, safe GPU buffer sharing) + v.m_modelMiddle = firstVolume->m_modelMiddle; + v.m_modelSmall = firstVolume->m_modelSmall; + // Share the readiness flags together with the models: while a + // flag is false its model may still be written by the background + // thread and must stay render-disabled. + v.m_lodMiddleReady = firstVolume->m_lodMiddleReady; + v.m_lodSmallReady = firstVolume->m_lodSmallReady; + // Note: model (main mesh) is always created per-volume since it's a value type + // This avoids dangling GPU buffer issues when one volume is destroyed + } + entry.volumes.emplace(&v); + } else { + MeshLodEntry entry; + entry.mesh = meshSharedPtr; // keep the mesh (and thus the map key) alive + entry.volumes.emplace(&v); + g_meshVolumesMap.emplace(meshPtr, std::move(entry)); + } + + // Always init the main model (GLModel is a value type, not shared) + const TriangleMesh& mesh = *meshPtr; #if ENABLE_SMOOTH_NORMALS v.model.init_from(mesh, true); #else - v.model.init_from(*mesh); + v.model.init_from(mesh); +#endif // ENABLE_SMOOTH_NORMALS + + // Generate LOD simplified models only once (shared via shared_ptr) + if (lodEnabled && !v.m_modelMiddle && !v.m_modelSmall) { + BOOST_LOG_TRIVIAL(info) << "LOD: Creating simplified models for '" << v.name + << "' faces=" << mesh.its.indices.size(); + v.m_modelMiddle = std::make_shared(); + // Keep rendering disabled until the background thread finishes + // init_from() (GLModel.hpp threading contract); the main thread + // re-enables it in promote_ready_lod_models() once the ready flag + // is observed. + v.m_modelMiddle->disable_render(); + v.m_lodMiddleReady = std::make_shared>(false); + v.SimplifyMesh(mesh, v.m_modelMiddle, v.m_lodMiddleReady, LODLevel::Middle); + + v.m_modelSmall = std::make_shared(); + v.m_modelSmall->disable_render(); + v.m_lodSmallReady = std::make_shared>(false); + v.SimplifyMesh(mesh, v.m_modelSmall, v.m_lodSmallReady, LODLevel::Small); + } else if (!lodEnabled) { + BOOST_LOG_TRIVIAL(info) << "LOD: Disabled for '" << v.name << "'"; + } + if (need_raycaster) { - v.mesh_raycaster = std::make_unique(mesh); + v.mesh_raycaster = std::make_unique(meshSharedPtr); } -#endif // ENABLE_SMOOTH_NORMALS v.composite_id = GLVolume::CompositeID(obj_idx, volume_idx, instance_idx); if (model_volume->is_model_part()) { @@ -754,6 +1173,21 @@ GLVolume* GLVolumeCollection::new_nontoolpath_volume(const ColorRGBA& rgba) return out; } +void GLVolumeCollection::release_volume(GLVolume* volume) +{ + if (volume == nullptr || volume->m_oriMesh == nullptr) + return; + auto iter = g_meshVolumesMap.find(volume->m_oriMesh); + if (iter == g_meshVolumesMap.end()) + return; + MeshLodEntry& entry = iter->second; + entry.volumes.erase(volume); + if (entry.volumes.empty()) + // Last holder is gone: drop the entry together with its owning + // reference to the mesh, so the key address can be reused safely. + g_meshVolumesMap.erase(iter); +} + GLVolumeWithIdAndZList volumes_to_render(const GLVolumePtrs& volumes, GLVolumeCollection::ERenderType type, const Transform3d& view_matrix, @@ -827,6 +1261,39 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, if (disable_cullface) glsafe(::glDisable(GL_CULL_FACE)); + // Set static camera state for LOD evaluation in GLVolume rendering + GLVolume::s_curZoom = camera.get_zoom(); + GLVolume::s_curViewProjMatrix = (projection_matrix.matrix() * view_matrix.matrix()).eval(); + GLVolume::s_curViewport = camera.get_viewport(); + + // Evaluate LOD level for each volume once per frame + float curZoom = GLVolume::s_curZoom; + bool shouldEvaluate = (std::abs(curZoom - GLVolume::s_lastCameraZoomValue) > ZOOM_THRESHOLD); + if (shouldEvaluate) + { + GLVolume::s_lastCameraZoomValue = curZoom; + } + for (GLVolumeWithIdAndZ& volume : to_render) + { + GLVolume* v = volume.first; + // Hand over LOD models whose background initialization finished. + // Must run every frame, on the main thread only. + v->promote_ready_lod_models(); + if (!v->picking && (shouldEvaluate || ++v->m_lodUpdateIndex >= LOD_UPDATE_FREQUENCY)) + { + v->m_lodUpdateIndex = 0; + LODLevel prevLod = v->m_curLodLevel; + v->m_curLodLevel = CalcVolumeBoxInScreenBiggerThanThreshold( + v->transformed_bounding_box(), GLVolume::s_curViewProjMatrix, + GLVolume::s_curViewport[2], GLVolume::s_curViewport[3]); + if (prevLod != v->m_curLodLevel) { + BOOST_LOG_TRIVIAL(debug) << "LOD level changed: " << static_cast(prevLod) + << " -> " << static_cast(v->m_curLodLevel) + << " (zoom=" << curZoom << ", name=" << v->name << ")"; + } + } + } + for (GLVolumeWithIdAndZ& volume : to_render) { //CPU Frustum culling auto _worldAABB = volume.first->transformed_bounding_box(); diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index bd526190be8..24c11b9bbc7 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -15,6 +15,7 @@ #include "GLShader.hpp" #include "MeshUtils.hpp" +#include #include #include @@ -62,6 +63,13 @@ using ModelObjectPtrs = std::vector; // Return appropriate color based on the ModelVolume. extern ColorRGBA color_from_model_volume(const ModelVolume& model_volume); +// LOD (Level of Detail) rendering optimization +enum class LODLevel { + High, // Original full-resolution mesh + Middle, // Medium simplification + Small, // High simplification +}; + class GLVolume { public: std::string name; @@ -84,6 +92,12 @@ class GLVolume { static float explosion_ratio; static float last_explosion_ratio; + // Cached camera state for LOD evaluation (set before render) + static float s_lastCameraZoomValue; + static float s_curZoom; + static Matrix4d s_curViewProjMatrix; + static std::array s_curViewport; + enum EHoverState : unsigned char { HS_None, @@ -138,6 +152,22 @@ class GLVolume { // Color used to render this volume. ColorRGBA render_color; + // LOD (Level of Detail) rendering + // Each LOD level has its own simplified model; shared across volumes with the same mesh + mutable LODLevel m_curLodLevel{ LODLevel::High }; + mutable unsigned char m_lodUpdateIndex{ 0 }; + std::shared_ptr m_modelMiddle; + std::shared_ptr m_modelSmall; + // Completion flags for the background simplification threads. The worker + // stores true (release) as its last touch of the LOD model; the main + // thread loads it (acquire) in promote_ready_lod_models() and only then + // calls enable_render() and starts using the model. Shared alongside the + // models so volumes sharing a LOD model share its readiness too. + std::shared_ptr> m_lodMiddleReady; + std::shared_ptr> m_lodSmallReady; + const TriangleMesh* m_oriMesh{ nullptr }; + std::pair m_tvertsRangeLod; + struct CompositeID { CompositeID(int object_id, int volume_id, int instance_id) : object_id(object_id), volume_id(volume_id), instance_id(instance_id) {} CompositeID() : object_id(-1), volume_id(-1), instance_id(-1) {} @@ -330,11 +360,14 @@ class GLVolume { //BBS: add simple render function for thumbnail void simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_objects, std::vector& extruder_colors, bool ban_light =false); - void set_bounding_boxes_as_dirty() { - m_transformed_bounding_box.reset(); - m_transformed_convex_hull_bounding_box.reset(); - m_transformed_non_sinking_bounding_box.reset(); - } + // LOD mesh simplification (async, uses quadric edge collapse) + bool SimplifyMesh(const TriangleMesh& mesh, std::shared_ptr model, std::shared_ptr> readyFlag, LODLevel lod) const; + bool SimplifyMesh(const indexed_triangle_set& its, std::shared_ptr model, std::shared_ptr> readyFlag, LODLevel lod) const; + // Main-thread handoff: enable rendering of LOD models whose background + // initialization has completed (see SimplifyMesh). Call once per frame. + void promote_ready_lod_models(); + + void set_bounding_boxes_as_dirty(); bool is_sla_support() const; bool is_sla_pad() const; @@ -438,7 +471,8 @@ class GLVolumeCollection const std::vector &instance_idxs, const std::string &color_by, bool opengl_initialized, - bool need_raycaster = true); + bool need_raycaster = true, + bool lodEnabled = true); int load_object_volume( const ModelObject *model_object, @@ -449,7 +483,8 @@ class GLVolumeCollection bool opengl_initialized, bool in_assemble_view = false, bool use_loaded_id = false, - bool need_raycaster = true); + bool need_raycaster = true, + bool lodEnabled = true); // Load SLA auxiliary GLVolumes (for support trees or pad). void load_object_auxiliary( const SLAPrintObject *print_object, @@ -475,8 +510,11 @@ class GLVolumeCollection bool partly_inside_enable =true ) const; - // Clear the geometry - void clear() { for (auto *v : volumes) delete v; volumes.clear(); } + // Clear the geometry. Volumes are unregistered from the LOD sharing map + // (release_volume) before being deleted. + void clear() { for (auto *v : volumes) { release_volume(v); delete v; } volumes.clear(); } + + void release_volume(GLVolume* volume); bool empty() const { return volumes.empty(); } void set_range(double low, double high) { for (GLVolume *vol : this->volumes) vol->set_range(low, high); } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 3376c117fe8..b4967a64889 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -34,7 +34,11 @@ #include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp" #include "slic3r/Utils/UndoRedo.hpp" +#include "slic3r/Utils/CpuMemory.hpp" #include "slic3r/Utils/MacDarkMode.hpp" +#ifdef __APPLE__ +#include "libslic3r/MacUtils.hpp" +#endif #include @@ -3387,22 +3391,22 @@ void GLCanvas3D::set_volumes_z_range(const std::array& range) m_volumes.set_range(range[0] - 1e-6, range[1] + 1e-6); } -std::vector GLCanvas3D::load_object(const ModelObject& model_object, int obj_idx, std::vector instance_idxs) +std::vector GLCanvas3D::load_object(const ModelObject& model_object, int obj_idx, std::vector instance_idxs, bool lodEnabled) { if (instance_idxs.empty()) { for (unsigned int i = 0; i < model_object.instances.size(); ++i) { instance_idxs.emplace_back(i); } } - return m_volumes.load_object(&model_object, obj_idx, instance_idxs, m_color_by, m_initialized); + return m_volumes.load_object(&model_object, obj_idx, instance_idxs, m_color_by, m_initialized, true, lodEnabled); } -std::vector GLCanvas3D::load_object(const Model& model, int obj_idx) +std::vector GLCanvas3D::load_object(const Model& model, int obj_idx, bool lodEnabled) { if (0 <= obj_idx && obj_idx < (int)model.objects.size()) { const ModelObject* model_object = model.objects[obj_idx]; if (model_object != nullptr) - return load_object(*model_object, obj_idx, std::vector()); + return load_object(*model_object, obj_idx, std::vector(), lodEnabled); } return std::vector(); @@ -3593,6 +3597,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re // BBS if (volume->is_wipe_tower) deleted_wipe_towers.emplace_back(volume, volume_id); + // Unregister from the LOD sharing map before the pointer + // becomes dangling (P0-2). + m_volumes.release_volume(volume); delete volume; } } @@ -3681,6 +3688,23 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re } } m_volumes.volumes = std::move(glvolumes_new); + + // LOD rendering optimization is always enabled + bool enableLod = true; + + // Disable LOD if free memory is less than 5GB + if (enableLod && CpuMemory::CurFreeMemoryLessThanSpecifySizeGb(LOD_FREE_MEMORY_SIZE)) + { + enableLod = false; + } + +#ifdef __APPLE__ + // Disable LOD on macOS 15 due to known rendering compatibility issues + if (Slic3r::IsMacVersion15()) { + enableLod = false; + } +#endif + for (unsigned int obj_idx = 0; obj_idx < (unsigned int)m_model->objects.size(); ++ obj_idx) { const ModelObject &model_object = *m_model->objects[obj_idx]; for (int volume_idx = 0; volume_idx < (int)model_object.volumes.size(); ++ volume_idx) { @@ -3701,7 +3725,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re // Note the index of the loaded volume, so that we can reload the main model GLVolume with the hollowed mesh // later in this function. it->volume_idx = m_volumes.volumes.size(); - m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx, m_color_by, m_initialized, m_canvas_type == ECanvasType::CanvasAssembleView); + m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx, m_color_by, m_initialized, m_canvas_type == ECanvasType::CanvasAssembleView, false, true, enableLod); m_volumes.volumes.back()->geometry_id = key.geometry_id; update_object_list = true; } else { diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 863e01ac831..60a8cf4fb16 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -1003,8 +1003,8 @@ class GLCanvas3D std::vector& get_custom_gcode_per_print_z() { return m_gcode_viewer.get_custom_gcode_per_print_z(); } size_t get_gcode_extruders_count() { return m_gcode_viewer.get_extruders_count(); } - std::vector load_object(const ModelObject& model_object, int obj_idx, std::vector instance_idxs); - std::vector load_object(const Model& model, int obj_idx); + std::vector load_object(const ModelObject& model_object, int obj_idx, std::vector instance_idxs, bool lodEnabled = true); + std::vector load_object(const Model& model, int obj_idx, bool lodEnabled = true); void mirror_selection(Axis axis); diff --git a/src/slic3r/Utils/CpuMemory.cpp b/src/slic3r/Utils/CpuMemory.cpp new file mode 100644 index 00000000000..831ba29e498 --- /dev/null +++ b/src/slic3r/Utils/CpuMemory.cpp @@ -0,0 +1,85 @@ +#include "CpuMemory.hpp" + +// Platform headers must stay outside namespace Slic3r: including them inside +// a namespace puts their declarations into that namespace and breaks (or +// silently depends) on the SDK's internal structure. +#ifdef _WIN32 +#include +#endif + +#ifdef __linux__ +#include +#include +#elif defined(__APPLE__) +#include +#include +#endif + +namespace Slic3r { +#ifdef _WIN32 +unsigned long long GetFreeMemoryWin() +{ + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); + GlobalMemoryStatusEx(&status); + return status.ullAvailPhys; +} +#endif + +#if defined(__linux__) || defined(__APPLE__) +unsigned long long GetFreMemoryUnix() +{ +#ifdef __linux__ + struct sysinfo info; + if (sysinfo(&info) == 0) { + return info.freeram * info.mem_unit; + } +#elif __APPLE__ + int mib[2] = {CTL_HW, HW_MEMSIZE}; + uint64_t memsize; + size_t len = sizeof(memsize); + if (sysctl(mib, 2, &memsize, &len, NULL, 0) == 0) { + vm_size_t page_size; + mach_port_t mach_port; + mach_msg_type_number_t count; + vm_statistics64_data_t vm_stats; + + mach_port = mach_host_self(); + count = sizeof(vm_stats) / sizeof(natural_t); + if (host_page_size(mach_port, &page_size) == KERN_SUCCESS && host_statistics64(mach_port, HOST_VM_INFO, (host_info64_t) &vm_stats, &count) == KERN_SUCCESS) { + return (vm_stats.free_count + vm_stats.inactive_count) * page_size; + } + } +#endif + return 0; +} +#endif +unsigned long long get_free_memory() +{ +#ifdef _WIN32 + return GetFreeMemoryWin(); +#elif defined(__linux__) || defined(__APPLE__) + return GetFreMemoryUnix(); +#else + return 0; +#endif +} +bool CpuMemory::CurFreeMemoryLessThanSpecifySizeGb(int size) +{ + unsigned long long free_mem = get_free_memory(); + auto cur_size = free_mem / (1024.0 * 1024.0 * 1024.0); + static bool first_debug_free_memory = true; + static bool first_meet_size_gb = true; + if (first_debug_free_memory) { + first_debug_free_memory = false; + } + if (cur_size < size) { + if (first_meet_size_gb) { + first_meet_size_gb = false; + } + return true; + } + return false; +} + +} diff --git a/src/slic3r/Utils/CpuMemory.hpp b/src/slic3r/Utils/CpuMemory.hpp new file mode 100644 index 00000000000..dd350302d40 --- /dev/null +++ b/src/slic3r/Utils/CpuMemory.hpp @@ -0,0 +1,18 @@ +#ifndef slic3r_CpuMemory_hpp_ +#define slic3r_CpuMemory_hpp_ + +namespace Slic3r { + +// Minimum free memory required for LOD rendering (in GB) +#define LOD_FREE_MEMORY_SIZE 5 + +class CpuMemory +{ +public: + // Returns true if current free memory is less than the specified size in GB + static bool CurFreeMemoryLessThanSpecifySizeGb(int sizeGb); +}; + +} // namespace Slic3r + +#endif