Question about UI lock ups when running a python script

Hey, maybe not the right place for a question but I can’t really find an answer elsewhere. My question is very simple: is it possible to run a python script and NOT lock up the user interface? I want to send a message about the progress to UI but operator report() function does not work until the processing is finished. I browsed a number of threads on Blender Stack Exchange but answers are partly incorrect or solutions are so overcomplicated that they are not worth investigating even if they do work… Could devs give a decisive answer? Is it possible to (programmatically) interact with UI while script is running or is this totally impossible?

Only if the script has regular breaks using the sleep function from time module, and even then the functions need to run in a separate thread.

Quick example:

import bpy

# report back to operator
def separate_thread(self):
    from time import sleep
    for i in range(100):
        sleep(0.1)
        self.report({'INFO'}, f"{i}%")
    self.report({'INFO'}, "100% - Finished")
    self.finished = True

class WM_OT_dummy_progress(bpy.types.Operator):
    bl_idname = "wm.dummy_progress"
    bl_label = "Dummy Progress"
    
    # keep operator alive using modal while function runs
    def modal(self, context, event):
        if self.finished:
            return {'FINISHED'}
        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        self.finished = False
        
        from threading import Timer
        Timer(0, separate_thread, (self,)).start()

        wm = context.window_manager
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}
    
def register():
    bpy.utils.register_class(WM_OT_dummy_progress)

if __name__ == '__main__':
    register()
    
    bpy.ops.wm.dummy_progress('INVOKE_DEFAULT')
2 Likes

@kaio thank you so much for your effort. That’s exactly one of the solutions that I call overly complicated… I am in the last days of building my add-on, and I am desperately fighting to get the UI reporting. Yesterday I managed to get it with bpy.wm.redraw_timer(). This function effectivley forces redrawing the screen. I did reporting with blf and forced updating. see here. However, I have discovered today that this stops working if you switch to another app and go back to Blender! From happiness to being desperate again… so I am just so exhausted now that I fear trying out your method and discrovering it does not suit my situation. To me your answer sounds like: no, there is no simple way to do UI reporting in Blender, to achieve this simlest thing, you actually have to do some multithreading and black magic. For now I just want to go with no reporting because, to be honest, that’s like building a nuclear reactor to cook some soup… Do you know where I can raise the topic and get some attention from devs? As a non-programmer with experience with other APIs, I think Blender’s API is pretty awful, and this would be a great step towards simplifying it and making it more friendly.

Well, to be fair, you asked if it was impossible. It isn’t, really. It just depends on how bad you want it.

The main issue is that python doesn’t run multi threaded. If no breaks are programmed in, the script keeps a lock until it finishes. While Blender’s core is C, the ui is drawn using python. I do agree that this isn’t optimal, but there are ways around it. This forum is probably the place you most likely get a definitive answer to this.

@kaio fair enough. just one last question: could you take a look at my case shortly? If I have simple operator that launches a series of bakes, and I want to throw a message before each new bake starts, you say it is possible to program a break before each bake and call another reporting thread following your example? do I understand correctly?
anyways, I appreciate your help. I’ll give it a try. Probably, this even deserves being documented propertly because I saw hundreds people having the same problem

now I see, by the way. I didn’t realize that UI is fully Python. Now I understand that that’s a kind of problem to implement that…

@kaio I guess I was too fast with my conclusions. Your example seems to do what I need. Don’t want to be overoptimistic but I’m pretty sure it’ll do the job. And it’s not too difficult… Thanks a lot!! I am going to publish a small tutorial after I am finished

Baking is probably not safe enough to run in a separate thread. The only way to not have it block the ui afaik is to invoke it the same way the ui does, using ‘INVOKE_DEFAULT’.

Still, in order to run successive bakes while maintaining ui responsiveness, they need to be wrapped in a macro. And the macro needs to be refreshed by a modal, otherwise it will wait until the user moves the mouse before executing the next bake.

With the example below, hopefully you’ve not been scared off.

  1. Add an image texture node to the default cube
  2. Add a new image data-block to the image node
  3. Run this script
  4. Search and run Bake Modal (use cycles)
import bpy, _bpy
from bpy.types import Operator

def init_macro():
    from bpy.types import Macro
    from bpy.utils import (
        register_class,
        unregister_class)

    class OBJECT_OT_bake_macro(Macro):
        bl_idname = "object.bake_macro"
        bl_label = "Bake Macro"

    class WM_OT_set_finished(Operator):
        bl_idname = "wm.bake_set_finished"
        bl_label = "Bake Set Finished"
        bl_options = {'INTERNAL'}

        def execute(self, context):
            dns = bpy.app.driver_namespace
            dns['bake_set_finished'] = True
            return {'FINISHED'}

    # need to re-register macro to support
    # changing sub-operator properties
    if hasattr(bpy.types, "OBJECT_OT_bake_macro"):
        unregister_class(bpy.types.OBJECT_OT_bake_macro)

    register_class(OBJECT_OT_bake_macro)
    if not hasattr(bpy.types, "WM_OT_set_finished"):
        register_class(WM_OT_set_finished)
    return bpy.types.OBJECT_OT_bake_macro

# main baking operator
class WM_OT_bake_modal(Operator):
    bl_idname = "wm.bake_modal"
    bl_label = "Bake Modal"

    def modal(self, context, event):

        if self.dns.get('bake_set_finished'):
            wm = context.window_manager
            wm.event_timer_remove(self.refresh)
            self.report({'INFO'}, "Finished baking")
            del bpy.app.driver_namespace['bake_set_finished']
            return {'FINISHED'}

        return {'PASS_THROUGH'}

    def invoke(self, context, event):

        macro = init_macro()

        # store a flag in dns so the modal knows when to end
        dns = bpy.app.driver_namespace
        dns['bake_set_finished'] = False
        self.dns = dns

        num_bakes = 2
        sub_op = 'OBJECT_OT_bake'
        define = _bpy.ops.macro_define

        for i in range(num_bakes):
            # sub-operators can be stored on the macro itself
            setattr(macro, f"bake_{i}", define(macro, sub_op))

        # define a last sub-op that tells the modal the bakes are done
        define(macro, 'WM_OT_bake_set_finished')

        # set some operator property for the first bake
        macro.bake_1.properties.margin = 24

        # 'INVOKE_DEFAULT' keeps the ui responsive. this is propagated onto the sub-ops
        bpy.ops.object.bake_macro('INVOKE_DEFAULT')

        wm = context.window_manager        
        # timer event needed to refresh the macro between bakes
        self.refresh = wm.event_timer_add(0.1, window=context.window)
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}

def register():
    bpy.utils.register_class(WM_OT_bake_modal)

if __name__ == '__main__':
    register()
2 Likes

@kaio would you mind answering some questions? Becuase either I am blind or there is NO documentation for 80 % of what you’ve written.

  1. you write in your for loop:
    macro.bake_i = define(macro, sub_op)
    but then you go
    define(macro, 'WM_OT_bake_set_finished')
    So, what does define() exactly do? It associates a given macro with a given operator?

  2. bpy.ops.object.bake_macro('INVOKE_DEFAULT') —> does this mean that Macro is an operator itself if it can be called like that?

  3. so, say, to bake different inputs of BSDF, I could have something like:

for i in range(len(BAKE_INPUTS)):
    setattr(macro, f"bake_{i}", define(macro, sub_op))
    setattr(macro, f"bake_{i}.input", BAKE_INPUTS[i])

like this? And then I define the sub_op as a simple operator doing the actual bake?

I defined bake macros in a for loop and format their variables. It’s just dynamic and short hand for:

macro.bake_1 = define(macro, sub_op)
macro.bake_2 = define(macro, sub_op)

Define is a function on the Macro class. A Macro operator is an empty container when you create it. We use define() to add sub-operators to it. The version I used is a generic one that found in the hidden _bpy module.

When you register a macro, it will automatically have a define() function that takes one parameter, the sup-operator. So writing

macro.define('OBJECT_OT_sub_operator')

would have also worked, but the define functions is here tied to the class. The define I used takes two parameters, a macro and a sub-operator.

When you define a sub-operator, it is returned and you can access the properties of the sub-operator so it runs with different settings, eg. bake type, margin, etc.:

macro.bake_1 = define('OBJECT_OT_bake')
macro.bake_1.properties.some_bake_property = some_setting

The macro can be called, but it doesn’t return or take any explicit parameters. I haven’t tested much (macro documentation is bad), but it does pass the execution context to its sub-operators.

The f"bake{i}" is just a dynamic way of assigning a variable to the sub-ops instead of explicitly typing bake_1, bake_2, bake_3. The f"{i}" implies the number from range(num_bakes) in the current loop index.

If you plan on changing the node setup between bakes, the changes need to happen before the next bake starts and be on their own custom sub-operator. Using a loop to define the macro woudn’t work well here so we define it explicitly instead:

# define the first bake, then the change_node_setup right after
setattr(macro, "bake_1", define(macro, sub_op))  # < just a regular bake sub-op
setattr(macro, "change_node_setup_1" define(macro, "BAKE_OT_change_settings"))  # < change settings sub-op

# change settings to the sub-op before next bake is defined.
macro.change_node_setup_1.properties.use_diffuse = True
macro.change_node_setup_1.properties.some_value = 0.75

# define the next bake
setattr(macro, "bake_2", define(macro, sub_op))  # < bake sub-op 2 using the new node setup
1 Like

@kaio

Define is a function on the Macro class. A Macro operator is an empty container when you create it. We use define() to add sub-operators to it. The version I used is a generic one that found in the hidden _bpy module.

Ok, if I understand correctly, the operators added to a Macro create a queue, and, when I call the macro, they will be executed in a queue. In your example, bake_1 sub_op will be executed first with parameters than you can pass to it, bake_2 will be executed second, WM_OT_bake_set_finisehd will be executed last. You are assigning them as a property to the macro to be able to access their properties and due to the fact than define() returns the operator itself… and that’s why you are NOT assigning the ‘WM_OT_bake_set_finisehd’ as a property: because there are no props that you might want to change. That’s clear now, thanks

If you plan on changing the node setup between bakes, the changes need to happen before the next bake starts and be on their own custom sub-operator . Using a loop to define the macro woudn’t work well here so we define it explicitly instead:

Okay, I didn’t fully get that but let me actually try to implement this first. For now I have an operator that’s already working. It has the only parameter basically - input type (‘Base Color’, ‘Metallic’, and so on). Then, when it runs, it would reshuffle nodes, save images, and bake. I am going to try to pass it to the Macro with different input types. I mean, instead of ‘OBJECT_OT_bake’ in your example I have ‘MY_BAKE_OT_0’ which does everything (node setup, create images, bake, save images, delete images). I then pass it to the macro queue like that:

INPUTS_BAKE = [
    'BaseColor',
    'Metallic', 
    'Normal'
]
# then in the invoke() function of the modal operator:
for inp in INPUTS_BAKE:
    setattr(macro, "bake_{}".format(inp), define(macro, MY_BAKE_OT_0)# <-- this 
# will add my baking sub operator to the macro queue three times, for three inputs
# for now these added suboperators all have same settings, which is handled below
    
# set custom properties of MY_BAKE_OT_0
macro.bake_BaseColor.props.bakeWhat = 'Base Color'
macro.bake_BaseColor.props.clean_up_after_Bake = True
# and then finally bpy.ops.object.obj_macro('INVOKE_DEFAULT')

That was a hell of an explanation, thank you! Let me try my implementation and get back here.
By the way, what is your method of exploring the Blender API. There is scarce to no documentation on things you are using but you still have a pretty good idea of them. Where is the secret?

No secret. Just an unhealthy amount of exploring the interactive console.

A lot of the otherwise obscured internals are accessible in python, but hidden away in private modules, special variables or lookup functions. You can get some hints of them by reading the source on the default py files used to draw the interface.

The exploring itself is a direct result of a void in the api docs :stuck_out_tongue:

1 Like

@kaio my deepest respect, that’s an outstanding example of persistance… and you’ve chosen the good word - void. I can imagine that exploration may be interesting but, unfortunately, oftentimes I just find it frustrating. I guess, it’s always better to have a good and clear guidance. I’m always somehow uncomfortable with the idea that you spend lifetime on something you could otherwise read and understand in minutes.
you’re doing Blender professionally? this is your only software or are you using something else? sorry, not really sure whether this is a place for off-topic talks

1 Like

@kaio - seems like you have a lot you could contribute to Blender by helping to improve the Python API documentation. If you are so inclined.

4 Likes