blenderpython/suw_core/memory_manager.py

583 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()