Blender脚本中revert_mainfile()内存存活与AV崩溃问题排查
问题:Blender重载Blend文件后二次迭代触发Access Violation崩溃
我尝试在Blender脚本的循环中重载blend文件,发现模态运算符在场景重载后实例会失效。于是自定义了一个类,将循环放在生成器中,调用revert_mainfile()后通过Blender的load_post、scene_update_pre链式回调恢复控制。第一次执行正常,但第二次迭代时Blender因Access Violation(AV)崩溃。调试发现scene_update_callback()最后一行执行后无返回目标,直接触发AV错误导致Blender关闭。
相关代码
""" The script shows how to have the iterator with Operator.modal() iterating with stored generator function to be able to reload blend file and wait for blender to load the scene asynchronously with callback to have a valid context to execute the next iteration. see also: https://blender.stackexchange.com/questions/17960/calling-delete-operator-after-revert-mainfile Sadly, a modal operator doesn't survive the scene reload, so use stored runner instance, getting control back with callbacks. """ import bpy #from bpy.props import IntProperty, FloatProperty import time from bpy.app.handlers import persistent ITERATIONS = 3 my_start_time = 0.0 # deb for my_report() @persistent def my_report(mess): print ("deb@ {:.3f} > {}".format(time.time() - my_start_time, mess)) @persistent def scene_update_callback(scene): """ this fires after actual scene reloading """ my_report("scene_update_callback start") # self-removal, only run once bpy.app.handlers.scene_update_pre.remove(scene_update_callback) # register that we have updated the scene MyShadowOperator.scene_loaded = True # emulate event for modal() MyShadowOperator.instance.modal() my_report("scene_update_callback finish") pass # after this, it seems we have nowhere to return, and an AV occurs :-( @persistent def load_post_callback(dummy): """ it's called immediately after revert_mainfile() before any actual loading """ my_report("load_post_callback") # self-removal, so it isn't called again bpy.app.handlers.load_post.remove(load_post_callback) # use a scene update handler to delay execution of code bpy.app.handlers.scene_update_pre.append(scene_update_callback) class MyShadowOperator(): """ This must survive after the file reload. """ instance = None scene_loaded = False def __init__(self, operator): __class__.instance = self # store all of the original operator parameters (in case of any) self.params = operator.as_keywords() # create the generator (no code from it is executing here) self.generator = self.processor() def modal(self): """ here we are on each iteration """ my_report("Shadow modal start") return next(self.generator) def processor(self): #, context """ this is a generator function to process chunks. returns {'FINISHED'} or {'RUNNING_MODAL'} """ my_report("processor init") for i in range(ITERATIONS): my_report(f"processor iteration step {i}") # here we reload the main blend file # first registering the callback bpy.app.handlers.load_post.append(load_post_callback) __class__.scene_loaded = False my_report("processor reloading the file") bpy.ops.wm.revert_mainfile() # the scene is already loaded my_report("processor after reloading the file") # yield something which will not confuse ops.__call__() if i == ITERATIONS-1: yield {} #'FINISHED' else: yield {} #'RUNNING_MODAL' return class ShadowOperatorRunner(bpy.types.Operator): """Few times reload original file, and create an object. """ bl_idname = "object.shadow_operator_runner" bl_label = "Simple Modal Operator" def execute(self, context): """ Here we start from the UI """ global my_start_time my_start_time = time.time() # deb my_report("runner execute() start") # create and init a shadow operator shadow = MyShadowOperator(self) # start the 1st iteration shadow.modal() # and just leave and hope the next execution in shadow via the callback my_report("runner execute() return") return {'FINISHED'} # no sense to keep it modal because it dies anyway def register(): bpy.utils.register_class(ShadowOperatorRunner) def unregister(): bpy.utils.unregister_class(ShadowOperatorRunner) if __name__ == "__main__": register() # test call bpy.ops.object.shadow_operator_runner('EXEC_DEFAULT')
控制台输出
deb@ 0.000 > runner execute() start deb@ 0.001 > Shadow modal start deb@ 0.001 > processor init deb@ 0.002 > processor iteration step 0 deb@ 0.003 > processor reloading the file Read blend: D:\mch\MyDocuments\scripts\blender\modal_operator_reloading_blend\da ta\runner_modal_operator.blend deb@ 0.026 > load_post_callback deb@ 0.027 > processor after reloading the file deb@ 0.028 > runner execute() return deb@ 0.031 > scene_update_callback start deb@ 0.031 > Shadow modal start deb@ 0.033 > processor iteration step 1 deb@ 0.034 > processor reloading the file Read blend: D:\mch\MyDocuments\scripts\blender\modal_operator_reloading_blend\da ta\runner_modal_operator.blend deb@ 0.057 > load_post_callback deb@ 0.060 > processor after reloading the file deb@ 0.060 > scene_update_callback finish Error : EXCEPTION_ACCESS_VIOLATION Address : 0x00007FF658DDC2F7 Module : D:\Program files\Blender Foundation\blender-2.79.0\blender.exe
Blender版本信息
Blender 2.79 (sub 7) build date: 28/07/2019 build time: 17:48 build commit date: 2019-06-27 build commit time: 10:41 build hash: e045fe53f1b0 build platform: Windows build type: Release
问题根源
- 回调执行栈被打断:
scene_update_callback是Blender内部事件驱动的回调,你直接在其中调用modal(),而modal()执行next(self.generator)后立刻触发revert_mainfile(),这个操作会强制重启场景加载流程,粗暴打断当前回调的执行栈,导致内存引用非法。 - 生成器执行时机错误:调用
revert_mainfile()后,生成器没有等待场景完全加载就继续执行后续代码(控制台输出的processor after reloading the file可证明),此时Blender内部状态未稳定,后续回调触发时会引用已失效的内存地址。 - 类实例状态脱钩:虽然用了
@persistent装饰器,但MyShadowOperator的类实例在场景重载后,内部生成器状态已和新场景脱钩,后续调用时出现非法内存访问。
修复方案
核心调整思路
让生成器在revert_mainfile()后暂停,直到场景完全加载完成再通过回调唤醒;避免在scene_update_pre回调中直接执行触发场景重载的操作,确保Blender完成当前回调流程后再处理下一步。
修复后的代码
""" 修复后的脚本:确保生成器在场景完全加载后再继续执行,避免回调栈被打断 """ import bpy import time from bpy.app.handlers import persistent ITERATIONS = 3 my_start_time = 0.0 # 调试计时 @persistent def my_report(mess): print ("deb@ {:.3f} > {}".format(time.time() - my_start_time, mess)) @persistent def scene_update_callback(scene): """场景加载完成后触发,唤醒生成器继续执行""" my_report("scene_update_callback start") # 移除自身,避免重复触发 bpy.app.handlers.scene_update_pre.remove(scene_update_callback) # 唤醒生成器 MyShadowOperator.instance.resume_generator() my_report("scene_update_callback finish") @persistent def load_post_callback(dummy): """文件重载后触发,注册场景更新回调等待场景稳定""" my_report("load_post_callback") bpy.app.handlers.load_post.remove(load_post_callback) bpy.app.handlers.scene_update_pre.append(scene_update_callback) class MyShadowOperator(): """持久化的操作器类,在场景重载后保持状态""" instance = None def __init__(self, operator): __class__.instance = self self.params = operator.as_keywords() self.generator = self.processor() def resume_generator(self): """从暂停处继续执行生成器""" try: next(self.generator) except StopIteration: my_report("所有迭代完成") def processor(self): """生成器:控制迭代流程,每次重载后暂停等待回调唤醒""" my_report("processor init") for i in range(ITERATIONS): my_report(f"processor iteration step {i}") # 注册回调,准备重载场景 bpy.app.handlers.load_post.append(load_post_callback) my_report("processor 开始重载文件") bpy.ops.wm.revert_mainfile() # 暂停生成器,等待场景加载完成后被回调唤醒 yield # 场景加载完成后,继续执行后续逻辑 my_report(f"processor 完成第{i}次重载,场景已稳定") # 最后一次迭代结束后清理 if i == ITERATIONS-1: my_report("迭代全部完成") class ShadowOperatorRunner(bpy.types.Operator): """多次重载原文件的测试操作器""" bl_idname = "object.shadow_operator_runner" bl_label = "Shadow Operator Runner" def execute(self, context): global my_start_time my_start_time = time.time() my_report("runner execute() start") shadow_operator = MyShadowOperator(self) # 启动第一次迭代 shadow_operator.resume_generator() my_report("runner execute() return") return {'FINISHED'} def register(): bpy.utils.register_class(ShadowOperatorRunner) def unregister(): bpy.utils.unregister_class(ShadowOperatorRunner) if __name__ == "__main__": register() bpy.ops.object.shadow_operator_runner('EXEC_DEFAULT')
关键修复点
- 生成器暂停机制:在
revert_mainfile()后添加yield,让生成器暂停执行,直到scene_update_callback调用resume_generator()才继续,确保场景完全加载后再执行后续代码。 - 回调职责分离:
scene_update_callback只负责唤醒生成器,不再直接执行打断栈的操作,让Blender完成当前回调流程后再处理下一步迭代。 - 移除无效状态标记:去掉
scene_loaded这类易导致状态混乱的静态变量,改用生成器的暂停/唤醒机制控制流程。
额外注意事项
- Blender 2.79的回调机制存在局限性,建议升级到Blender 3.x版本,其
load_post和depsgraph_update_post回调稳定性更高。 - 所有持久化回调必须用
@persistent装饰,否则场景重载后会被自动移除。
内容的提问来源于stack exchange,提问作者Mechanic
相关产品推荐
相关产品推荐

