You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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')

关键修复点

  1. 生成器暂停机制:在revert_mainfile()后添加yield,让生成器暂停执行,直到scene_update_callback调用resume_generator()才继续,确保场景完全加载后再执行后续代码。
  2. 回调职责分离:scene_update_callback只负责唤醒生成器,不再直接执行打断栈的操作,让Blender完成当前回调流程后再处理下一步迭代。
  3. 移除无效状态标记:去掉scene_loaded这类易导致状态混乱的静态变量,改用生成器的暂停/唤醒机制控制流程。

额外注意事项

  • Blender 2.79的回调机制存在局限性,建议升级到Blender 3.x版本,其load_post和depsgraph_update_post回调稳定性更高。
  • 所有持久化回调必须用@persistent装饰,否则场景重载后会被自动移除。

内容的提问来源于stack exchange,提问作者Mechanic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 14:25:23