GPU and DearPyGUI rendering on separate thread from Blender?

Long story short, I’m planning to make a custom UV viewer/editor with limited functionality (no mesh manipulation etc. just colors) for a larger addon in the works. This portion of the addon should only receives UV data from an array of objects and display it in dearpygui window.

The only thing I’m wondering, as the title suggests, if it’s possible to have both the gpu module and dearpygui portion of the code running on their own shared thread separate from the main-thread that Blender uses? Because AI’s telling me that the GPU or GL context can only exist on the main thread (where Blender runs) but when I read the documentation I’m getting the understanding that that’s not true at all, but I could be wrong.

For now I’m using gpu.types.GPUOffScreen for creating buffers necessary for rendering and then writing the rendered image into a buffer object applicable to be sent to dearypygui but I believe that this part is done on the CPU side, which is CPU intensive? Is there a way to skip this step and have a direct communication, or in another words, transfer of image by the FBO to dearpygui?

Or better yet, has anyone done anything similar? Can you give me any pointers how this is all properly done?

Here’s the code that runs on the main thread, am wondering if it’s possible to make it work on a separate thread. I realize that my writing might not be most coherent and sensical and that’s probably due to my lack of knowledge on the rendering subject as well as mutl-threading.

import gpu
import numpy as np
from mathutils import Matrix
from gpu_extras.presets import draw_circle_2d
import dearpygui.dearpygui as dpg
from gpu.types import Buffer

def render_offscreen_to_numpy(w=512, h=512):
    offscreen = gpu.types.GPUOffScreen(w, h)

    with offscreen.bind():
        # Draw into the full off‑screen buffer
        gpu.state.viewport_set(0, 0, w, h)
        fb = gpu.state.active_framebuffer_get()
        fb.clear(color=(0.0, 0.0, 0.0, 0.0))

        with gpu.matrix.push_pop():
            gpu.matrix.load_matrix(Matrix.Identity(4))
            gpu.matrix.load_projection_matrix(Matrix.Identity(4))

            amount = 10
            for i in range(-amount, amount + 1):
                x = i / amount
                draw_circle_2d((x, 0.0), (1, 1, 1, 1), 0.5, segments=128)

    # Read back as 8‑bit unsigned bytes into a GPUBuffer
    buf = Buffer('UBYTE', w * h * 4)
    fb.read_color(0, 0, w, h, 4, 0, 'UBYTE', data=buf)
    offscreen.free()

    # Convert buffer → NumPy array
    arr = np.fromiter(buf, count=w * h * 4, dtype=np.uint8)
    arr = arr.reshape((h, w, 4))

    # Flip vertically for Dear PyGui (top‑left origin)
    return np.flip(arr, axis=0)


def show_in_dearpygui(img: np.ndarray):
    h, w, _ = img.shape
    dpg.create_context()
    with dpg.texture_registry(show=False):
        tex = dpg.add_static_texture(w, h, img.flatten() / 255.0)

    with dpg.window(label="Off‑Screen → DPG", width=w + 20, height=h + 40):
        dpg.add_image(tex)

    dpg.create_viewport(title="Off‑Screen Render Preview", width=w + 40, height=h + 80)
    dpg.setup_dearpygui()
    dpg.show_viewport()
    while dpg.is_dearpygui_running():
        dpg.render_dearpygui_frame()
    dpg.destroy_context()

def main():
    img = render_offscreen_to_numpy(512, 512)
    show_in_dearpygui(img)

if __name__ == "__main__":
    main()

Genuinely disappointed that no one replied to the thread. However, in the meantime I believe I’ve actually found an appropriate and practical approach to tackling this problem and fulfilling all my requirements.

First things first, it’s not possible to use gpu module when utilizing a separate thread (multi-threading), since the context that manages drawing only exists on the main thread. Therefore you lose access to it when you’re on a separate thread. As already mentioned, this is a problem when you want to use both of your programs (Blender and the custom GUI one) at the same time.

Luckily there’s an OpenGL wrapper in form of a python module called ModernGL which is quite easy to use and allows for multi-threading.

TLDR;
All in all, for all unsuspecting victims who have fallen into this trap, I suggest using ModernGL in conjunction with DearPyGUI. It just works.

IMPORTANT DISCLAIMER:
Unfortunately, it turns out DearPyGUI suffers from lack of reading directly from GPU rendered textures, it’s only possible via CPU methods, which I strongly discourage anyone from using as its unpractically and introduces some other problems. For more information and solution head to this Github “discussion”.

Sometimes when you have a very technical question involving the intersection of two different very technical tools, the venn diagram of people who know both at the same time just looks like two circles that don’t touch.

Don’t take it personally, sometimes you are just the first person who has ever had this problem and only you are in the position to solve your problem.

No worries, I’m not mad or anything. While I was very disappointed at first, I soon realized the exact thing you’ve just described while scouvering throughout the internet, and that has put my mind at ease. I just hope this thread will help someone in the future.

You are a trail blazer!

Thanks for sharing your experience in uncharted territory!

Yeah, from what I understand about DearPyGui, it does not supply a context for drawing in. At least thats what the developer told me but that was a while back.

If you need a GUI based frame work within the python ecosystem that does have an OpenGL context, then use https://wxpython.org/index.html
WxWidgets is a boss lol.

It will give you a GL Canvas to work with.

Also… I dont really know what the offscreen rending was for?
If you want to pass data to an external app, “live link”, than you are going to want to use networking. The network drivers on you PC send and receive data packets and you can create a socket on and attach it to a port. Then that socket can have callbacks to handle the updates. (Not a networking person but in general thats where I would be looking)

I agree, that’s why I then first switched to pyimgui only to find out later in the line that it does not have an embeded color picker, but just a color editor which is not what I needed. Thankfully, imgui_bundle had exactly what I needed. Plus it has a really modernish look to it. Here’s a link to my implementation of it in action, it can be seen at 04:05 mark specifically.

Judging by their official site their UI out-of-the-box looks as outdated as it does in wxWidgets (C++), which is a shame, unless its looks can easily be modified, then it’s a whole different story. Don’t get me wrong, while I realize it’s powerful it’s just that I know from experience in working with wxWidgets and reading forum discussions online that people generaly throw a fit at its outdated design.

Yeah, I had no idea what I was doing at the time (I still don’t to a certain degree).

It looks like you got something working though.
There is also https://kivy.org
Thats a modern GUI for python using a shader based approach.

So what did you end up doing for data transfer?

Just got GPT to make this but it does work.
Edit* Looks like it might not shut down processes properly though lol

BLENDER

bl_info = {
    "name": "Socket Bridge Panel",
    "author": "You",
    "version": (1, 0),
    "blender": (4, 0, 0),
    "category": "3D View",
}

import bpy
import socket
import threading

HOST = "127.0.0.1"
PORT = 65432

clients = []

# --- Socket Server ---
def socket_server():
    """Starts a socket server that keeps connections open."""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((HOST, PORT))
    s.listen(5)
    print(f"[Blender] Socket server listening on {HOST}:{PORT}")
    while True:
        conn, addr = s.accept()
        print(f"[Blender] Connected by {addr}")
        clients.append(conn)

def send_to_clients(message: str):
    """Send message to all connected clients."""
    for c in clients[:]:
        try:
            c.sendall(message.encode("utf-8"))
        except Exception:
            clients.remove(c)

# Start the server on register
def start_server_once():
    if not getattr(start_server_once, "started", False):
        thread = threading.Thread(target=socket_server, daemon=True)
        thread.start()
        start_server_once.started = True

# --- Property and UI ---
def on_text_update(self, context):
    send_to_clients(self.socket_text)
    print(f"[Blender] Sent: {self.socket_text}")

class SocketBridgeProps(bpy.types.PropertyGroup):
    socket_text: bpy.props.StringProperty(
        name="Text",
        description="Text to send via socket",
        default="Hello Tkinter!",
        update=on_text_update
    )

class VIEW3D_PT_SocketBridge(bpy.types.Panel):
    bl_label = "Socket Bridge"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "SocketBridge"

    def draw(self, context):
        layout = self.layout
        props = context.scene.socket_bridge
        layout.prop(props, "socket_text")

# --- Register ---
classes = (SocketBridgeProps, VIEW3D_PT_SocketBridge)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.socket_bridge = bpy.props.PointerProperty(type=SocketBridgeProps)
    start_server_once()

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    del bpy.types.Scene.socket_bridge

if __name__ == "__main__":
    register()

APP

import socket
import threading
import tkinter as tk

HOST = "127.0.0.1"
PORT = 65432

root = tk.Tk()
root.title("Blender Socket Viewer")

label_var = tk.StringVar(value="Waiting for Blender...")

tk.Label(root, textvariable=label_var, font=("Segoe UI", 16)).pack(padx=20, pady=20)

def listen_for_messages():
    """Connects to the Blender socket and listens for messages."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        try:
            s.connect((HOST, PORT))
            print("[Tkinter] Connected to Blender socket.")
            while True:
                data = s.recv(1024)
                if not data:
                    break
                msg = data.decode("utf-8")
                label_var.set(msg)
        except ConnectionRefusedError:
            label_var.set("No connection to Blender.")
            print("[Tkinter] Could not connect.")
        except Exception as e:
            print("[Tkinter] Error:", e)

# Run socket listener in a thread
threading.Thread(target=listen_for_messages, daemon=True).start()

root.mainloop()

That’s really cool, thanks!

I used queue.