blenderpython/suw_core/memory_manager.py

583 lines
22 KiB
Python
Raw Normal View History

2025-08-01 17:13:30 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SUW Core - Memory Manager Module
拆分自: suw_impl.py (Line 82-605)
用途: Blender内存管理依赖图管理主线程处理
版本: 1.0.0
作者: SUWood Team
"""
import time
import logging
import threading
import queue
from typing import Dict, Callable
from contextlib import contextmanager
# 设置日志
logger = logging.getLogger(__name__)
# 检查Blender可用性
try:
import bpy
BLENDER_AVAILABLE = True
except ImportError:
BLENDER_AVAILABLE = False
# 全局主线程任务队列
_main_thread_queue = queue.Queue()
_main_thread_id = None
# ==================== 内存管理核心类 ====================
class BlenderMemoryManager:
"""Blender内存管理器 - 修复弱引用问题"""
def __init__(self):
# 改用普通集合和字典来跟踪对象,而不是弱引用
self.tracked_objects = set() # 存储对象名称而不是对象本身
self.tracked_meshes = set() # 存储网格名称
self.tracked_images = set() # 存储图像名称
self.tracked_materials = set() # 存储材质名称
self.tracked_collections = set() # 存储集合名称
self.cleanup_interval = 100
self.operation_count = 0
self.last_cleanup = time.time()
self.max_memory_mb = 2048
self._cleanup_lock = threading.Lock()
self.creation_stats = {
"objects_created": 0,
"objects_cleaned": 0
}
def register_object(self, obj):
"""注册对象到内存管理器 - 修复版本"""
if obj is None or not BLENDER_AVAILABLE:
return
try:
with self._cleanup_lock:
# 根据对象类型分别处理
if hasattr(obj, 'name'):
obj_name = obj.name
# 根据对象类型存储到不同的集合
if hasattr(obj, 'type'): # Blender Object
self.tracked_objects.add(obj_name)
elif str(type(obj)).find('Material') != -1: # Material
self.tracked_materials.add(obj_name)
elif str(type(obj)).find('Mesh') != -1: # Mesh
self.tracked_meshes.add(obj_name)
elif str(type(obj)).find('Image') != -1: # Image
self.tracked_images.add(obj_name)
elif str(type(obj)).find('Collection') != -1: # Collection
self.tracked_collections.add(obj_name)
else:
self.tracked_objects.add(obj_name)
self.operation_count += 1
# 定期清理
if self.should_cleanup():
self.cleanup_orphaned_data()
except Exception as e:
# 静默处理,不输出错误日志
pass
def register_mesh(self, mesh):
"""注册网格到内存管理器 - 修复版本"""
if mesh is None or not BLENDER_AVAILABLE:
return
try:
with self._cleanup_lock:
if hasattr(mesh, 'name'):
self.tracked_meshes.add(mesh.name)
self.operation_count += 1
except Exception as e:
# 静默处理
pass
def register_image(self, image):
"""注册图像到内存管理器 - 修复版本"""
if image is None or not BLENDER_AVAILABLE:
return
try:
with self._cleanup_lock:
if hasattr(image, 'name'):
self.tracked_images.add(image.name)
self.operation_count += 1
except Exception as e:
# 静默处理
pass
def should_cleanup(self):
"""检查是否需要清理"""
return (self.operation_count >= self.cleanup_interval or
time.time() - self.last_cleanup > 300) # 5分钟强制清理
def cleanup_orphaned_data(self):
"""【暂时禁用】清理孤立的数据块 - 让Blender自动处理以避免冲突"""
if not BLENDER_AVAILABLE:
return
# 【临时策略】完全禁用自动清理,只更新跟踪状态
logger.debug("🚫 自动清理已禁用让Blender自动处理孤立数据")
cleanup_count = 0
try:
with self._cleanup_lock:
# 只清理跟踪列表,不实际删除任何数据
invalid_objects = []
invalid_meshes = []
invalid_materials = []
invalid_images = []
# 清理无效的对象引用(不删除实际对象)
for obj_name in list(self.tracked_objects):
try:
if obj_name not in bpy.data.objects:
invalid_objects.append(obj_name)
except:
invalid_objects.append(obj_name)
# 清理无效的网格引用(不删除实际网格)
for mesh_name in list(self.tracked_meshes):
try:
if mesh_name not in bpy.data.meshes:
invalid_meshes.append(mesh_name)
except:
invalid_meshes.append(mesh_name)
# 清理无效的材质引用(不删除实际材质)
for mat_name in list(self.tracked_materials):
try:
if mat_name not in bpy.data.materials:
invalid_materials.append(mat_name)
except:
invalid_materials.append(mat_name)
# 清理无效的图像引用(不删除实际图像)
for img_name in list(self.tracked_images):
try:
if img_name not in bpy.data.images:
invalid_images.append(img_name)
except:
invalid_images.append(img_name)
# 只更新跟踪列表,不删除实际数据
for obj_name in invalid_objects:
self.tracked_objects.discard(obj_name)
for mesh_name in invalid_meshes:
self.tracked_meshes.discard(mesh_name)
for mat_name in invalid_materials:
self.tracked_materials.discard(mat_name)
for img_name in invalid_images:
self.tracked_images.discard(img_name)
total_cleaned = len(invalid_objects) + len(invalid_meshes) + \
len(invalid_materials) + len(invalid_images)
if total_cleaned > 0:
logger.debug(f"🧹 清理了 {total_cleaned} 个无效引用(不删除实际数据)")
# 【修复】安全清理材质数据
materials_to_remove = []
for material_name in list(self.tracked_materials):
try:
if material_name in bpy.data.materials:
material = bpy.data.materials[material_name]
if material.users == 0:
materials_to_remove.append(material_name)
else:
self.tracked_materials.discard(material_name)
except Exception as e:
logger.warning(f"检查材质 {material_name} 时出错: {e}")
self.tracked_materials.discard(material_name)
# 批量删除无用的材质
for material_name in materials_to_remove:
try:
if material_name in bpy.data.materials:
material = bpy.data.materials[material_name]
bpy.data.materials.remove(material, do_unlink=True)
cleanup_count += 1
self.tracked_materials.discard(material_name)
except Exception as e:
logger.warning(f"删除材质数据失败: {e}")
self.tracked_materials.discard(material_name)
# 【修复】安全清理图像数据
images_to_remove = []
for image_name in list(self.tracked_images):
try:
if image_name in bpy.data.images:
image = bpy.data.images[image_name]
if image.users == 0:
images_to_remove.append(image_name)
else:
self.tracked_images.discard(image_name)
except Exception as e:
logger.warning(f"检查图像 {image_name} 时出错: {e}")
self.tracked_images.discard(image_name)
# 批量删除无用的图像
for image_name in images_to_remove:
try:
if image_name in bpy.data.images:
image = bpy.data.images[image_name]
bpy.data.images.remove(image, do_unlink=True)
cleanup_count += 1
self.tracked_images.discard(image_name)
except Exception as e:
logger.warning(f"删除图像数据失败: {e}")
self.tracked_images.discard(image_name)
# 【修复】清理无效的对象引用
invalid_objects = []
for obj_name in list(self.tracked_objects):
try:
if obj_name not in bpy.data.objects:
invalid_objects.append(obj_name)
except Exception as e:
logger.warning(f"检查对象 {obj_name} 时出错: {e}")
invalid_objects.append(obj_name)
for obj_name in invalid_objects:
self.tracked_objects.discard(obj_name)
if cleanup_count > 0:
logger.info(f"🧹 清理了 {cleanup_count} 个孤立数据块")
except Exception as e:
logger.error(f"内存清理过程中发生错误: {e}")
import traceback
traceback.print_exc()
def _cleanup_tracked_references(self):
"""清理跟踪集合中的无效引用"""
try:
# 清理无效的对象引用
valid_objects = set()
for obj_name in self.tracked_objects:
if obj_name in bpy.data.objects:
valid_objects.add(obj_name)
self.tracked_objects = valid_objects
# 清理无效的网格引用
valid_meshes = set()
for mesh_name in self.tracked_meshes:
if mesh_name in bpy.data.meshes:
valid_meshes.add(mesh_name)
self.tracked_meshes = valid_meshes
# 清理无效的材质引用
valid_materials = set()
for mat_name in self.tracked_materials:
if mat_name in bpy.data.materials:
valid_materials.add(mat_name)
self.tracked_materials = valid_materials
# 清理无效的图像引用
valid_images = set()
for img_name in self.tracked_images:
if img_name in bpy.data.images:
valid_images.add(img_name)
self.tracked_images = valid_images
# 清理无效的集合引用
valid_collections = set()
if hasattr(bpy.data, 'collections'):
for col_name in self.tracked_collections:
if col_name in bpy.data.collections:
valid_collections.add(col_name)
self.tracked_collections = valid_collections
except Exception as e:
logger.warning(f"清理跟踪引用失败: {e}")
def get_memory_stats(self) -> Dict[str, int]:
"""获取内存使用统计"""
try:
stats = {
"manager_type": "BlenderMemoryManager", # 添加这个字段
"tracked_objects": len(self.tracked_objects),
"tracked_meshes": len(self.tracked_meshes),
"tracked_images": len(self.tracked_images),
"creation_count": self.creation_stats.get("objects_created", 0),
"cleanup_count": self.creation_stats.get("objects_cleaned", 0),
"blender_available": BLENDER_AVAILABLE
}
return stats
except Exception as e:
return {"manager_type": "BlenderMemoryManager", "error": str(e)}
def force_cleanup(self):
"""强制清理"""
try:
with self._cleanup_lock:
self.last_cleanup = 0 # 重置时间以强制清理
self.cleanup_orphaned_data()
except Exception as e:
logger.error(f"强制清理失败: {e}")
# ==================== 依赖图管理器 ====================
class DependencyGraphManager:
"""依赖图管理器 - 控制更新频率,避免过度更新导致的冲突"""
def __init__(self):
self.update_interval = 0.1 # 100毫秒最小更新间隔
self.last_update_time = 0
self.pending_updates = False
self._update_lock = threading.Lock()
self._updating = False # 【新增】防止递归更新的标志
def request_update(self, force=False):
"""请求依赖图更新 - 线程安全版本"""
if not BLENDER_AVAILABLE:
return
# 【新增】线程安全检查 - 只在主线程中执行更新
if threading.current_thread().ident != _main_thread_id:
logger.debug("跳过非主线程的依赖图更新")
self.pending_updates = True
return
with self._update_lock:
current_time = time.time()
if force or (current_time - self.last_update_time) >= self.update_interval:
try:
# 【修复依赖图冲突】添加求值状态检查
if hasattr(bpy.context, 'evaluated_depsgraph_get'):
# 检查是否在依赖图求值过程中
try:
depsgraph = bpy.context.evaluated_depsgraph_get()
if depsgraph.is_evaluating:
logger.debug("⚠️ 依赖图正在求值中,跳过更新")
return
except:
pass
# 【修复】使用延迟更新机制,避免递归调用
if not getattr(self, '_updating', False):
self._updating = True
try:
bpy.context.view_layer.update()
self.last_update_time = current_time
self.pending_updates = False
logger.debug("✅ 依赖图更新完成")
finally:
self._updating = False
else:
logger.debug("⚠️ 依赖图更新正在进行中,跳过")
except (AttributeError, ReferenceError, RuntimeError) as e:
# 这些错误在对象删除过程中是预期的
logger.debug(f"依赖图更新时的预期错误: {e}")
except Exception as e:
logger.warning(f"依赖图更新失败: {e}")
# 【新增】记录失败但不抛出异常
if hasattr(self, '_updating'):
self._updating = False
def flush_pending_updates(self):
"""强制执行所有挂起的更新"""
if self.pending_updates:
self.request_update(force=True)
# ==================== 主线程处理 ====================
def init_main_thread():
"""初始化主线程ID"""
global _main_thread_id
_main_thread_id = threading.current_thread().ident
def execute_in_main_thread_async(func: Callable, *args, **kwargs):
"""
真正的异步版在主线程中安全地调度函数 - 真正的"即发即忘"不等待结果
"""
global _main_thread_queue, _main_thread_id
# 如果已经在主线程中,直接执行
if threading.current_thread().ident == _main_thread_id:
try:
func(*args, **kwargs)
return True
except Exception as e:
logger.error(f"在主线程直接执行函数时出错: {e}")
import traceback
traceback.print_exc()
return False
# 在Blender中使用应用程序定时器 - 即发即忘模式
try:
import bpy
def timer_task():
try:
func(*args, **kwargs)
except Exception as e:
logger.error(f"主线程任务执行失败: {e}")
import traceback
traceback.print_exc()
return None # 只执行一次
# 注册定时器任务就立即返回,不等待结果
bpy.app.timers.register(timer_task, first_interval=0.001)
# !!!关键:立即返回调度成功,不等待执行结果!!!
return True
except ImportError:
# 不在Blender环境中使用原有的队列机制 - 也改为即发即忘
def wrapper():
try:
func(*args, **kwargs)
except Exception as e:
logger.error(f"队列任务执行失败: {e}")
import traceback
traceback.print_exc()
_main_thread_queue.put(wrapper)
# 立即返回调度成功,不等待执行结果
return True
# 【保持向后兼容】旧函数名的别名
execute_in_main_thread = execute_in_main_thread_async
def process_main_thread_tasks():
"""
修复版处理主线程任务队列 - 一次只处理一个任务
这个函数需要被Blender的定时器定期调用
"""
global _main_thread_queue
try:
# !!!关键修改:从 while 改为 if
# 一次定时器触发只处理队列中的一个任务然后就把控制权还给Blender。
if not _main_thread_queue.empty():
task = _main_thread_queue.get_nowait()
try:
task()
except Exception as e:
logger.error(f"执行主线程任务时出错: {e}")
import traceback
traceback.print_exc()
except queue.Empty:
pass # 队列是空的,什么也不做
@contextmanager
def safe_blender_operation(operation_name: str):
"""线程安全的Blender操作上下文管理器 - 修复版本"""
if not BLENDER_AVAILABLE:
logger.warning(f"Blender不可用跳过操作: {operation_name}")
yield
return
start_time = time.time()
logger.debug(f"🔄 开始操作: {operation_name}")
# 保存当前状态
original_mode = None
original_selection = []
original_active = None
def _execute_operation():
nonlocal original_mode, original_selection, original_active
try:
# 确保在对象模式下
if hasattr(bpy.context, 'mode') and bpy.context.mode != 'OBJECT':
original_mode = bpy.context.mode
bpy.ops.object.mode_set(mode='OBJECT')
# 保存当前选择和活动对象
if hasattr(bpy.context, 'selected_objects'):
original_selection = list(bpy.context.selected_objects)
if hasattr(bpy.context, 'active_object'):
original_active = bpy.context.active_object
# 清除选择以避免冲突
bpy.ops.object.select_all(action='DESELECT')
return True
except Exception as e:
logger.error(f"准备操作失败: {e}")
return False
def _cleanup_operation():
try:
# 尝试恢复原始状态
bpy.ops.object.select_all(action='DESELECT')
for obj in original_selection:
if obj and obj.name in bpy.data.objects:
obj.select_set(True)
# 恢复活动对象
if original_active and original_active.name in bpy.data.objects:
bpy.context.view_layer.objects.active = original_active
# 恢复模式
if original_mode and original_mode != 'OBJECT':
bpy.ops.object.mode_set(mode=original_mode)
except Exception as restore_error:
logger.warning(f"恢复状态失败: {restore_error}")
try:
# 如果不在主线程,使用主线程执行准备操作
if threading.current_thread().ident != _main_thread_id:
success = execute_in_main_thread(_execute_operation)
if not success:
raise RuntimeError("准备操作失败")
else:
success = _execute_operation()
if not success:
raise RuntimeError("准备操作失败")
# 执行用户操作
yield
elapsed_time = time.time() - start_time
if elapsed_time > 5.0:
logger.warning(f"操作耗时过长: {operation_name} ({elapsed_time:.2f}s)")
else:
logger.debug(f"✅ 操作完成: {operation_name} ({elapsed_time:.2f}s)")
except Exception as e:
logger.error(f"❌ 操作失败: {operation_name} - {e}")
raise
finally:
# 清理操作也需要在主线程中执行
if threading.current_thread().ident != _main_thread_id:
try:
execute_in_main_thread(_cleanup_operation)
except:
pass
else:
_cleanup_operation()
# ==================== 全局实例 ====================
# 全局内存管理器实例
memory_manager = BlenderMemoryManager()
# 全局依赖图管理器
dependency_manager = DependencyGraphManager()