0
\$\begingroup\$

I'm really new to this community. I'm sorry for any mistakes in advance.I'm making a game like minecraft with GLFW and OpenGL. The problem is, I just can't render a VBO while I update it from another thread. Here is my code:

main.py

# imports
import glfw
from OpenGL.GL import *
from OpenGL.GLU import *

# internal imports
from core.renderer import *
from terrain import *
from player import *

if not glfw.init():
    raise Exception("glfw can not be initialized!")

window = glfw.create_window(800, 500, "PyCraft", None, None)
glfw.make_context_current(window)
renderer = TerrainRenderer(window)
player = Player(window)

glEnable(GL_DEPTH_TEST)
glEnable(GL_CULL_FACE)
glCullFace(GL_BACK)
# glEnable(GL_FOG)
glFogfv(GL_FOG_COLOR, (GLfloat * int(8))(0.5, 0.69, 1.0, 10))
glHint(GL_FOG_HINT, GL_DONT_CARE)
glFogi(GL_FOG_MODE, GL_LINEAR)
glFogf(GL_FOG_START, 3)
glFogf(GL_FOG_END, 10)

renderer.texture_manager.add_from_folder("assets/textures/block/")
renderer.texture_manager.save("atlas.png")
renderer.texture_manager.bind()

world = World(renderer, player)
world.generate()

# get window size
def get_window_size():
    width, height = glfw.get_window_size(window)
    return width, height

def _setup_3d():
    w, h = get_window_size()

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(70, w / h, 0.1, 1000)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

def update_on_resize():
    _setup_3d()
    glViewport(0, 0, *get_window_size())

# mainloop
while not glfw.window_should_close(window):
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    update_on_resize()

    _setup_3d()
    glClearColor(0.5, 0.7, 1, 1.0)

    player.update()
    player._translate()

    glfw.poll_events()
    glfw.swap_buffers(window)

glfw.terminate()

renderer.py:

# imports
import glfw, numpy
from OpenGL.GL import *
from ctypes import *
from core.texture_manager import *

glfw.init()

class VBOManager:
    def __init__(self, renderer):
        self.renderer = renderer
        self.run()
    
    def run(self):
        for i in self.renderer.to_add[:self.renderer.to_add_count]:
            self.renderer.vertices.extend(i[0])
            self.renderer.texCoords.extend(i[1])

            _ = i
            self.renderer.to_add.remove(i)

            glBindBuffer(GL_ARRAY_BUFFER, self.renderer.vbo)
            glBufferSubData(GL_ARRAY_BUFFER, len(self.renderer.vertices), len(_[0]) * 4, (GLfloat * len(_[0]))(*_[0]))
            glFlush()
            glVertexPointer(3, GL_FLOAT, 0, None)
            glTexCoordPointer(3, GL_FLOAT, 0, None)
            glBindBuffer(GL_ARRAY_BUFFER, self.renderer.vbo_1)
            glBufferSubData(GL_ARRAY_BUFFER,  len(self.renderer.texCoords), len(_[1]) * 4, (GLfloat * len(_[1]))(*_[1]))
            glFlush()

class TerrainRenderer:
    def __init__(self, window):
        self.window = window

        self.vertices = []
        self.texCoords = []

        self.to_add = []
        self.to_add_count = 256

        self.vbo, self.vbo_1 = glGenBuffers (2)
        glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
        glBufferData(GL_ARRAY_BUFFER, 12 * 4, None, GL_STATIC_DRAW)
        self.vbo_manager = VBOManager(self)

        self.texture_manager = TextureAtlas()

        glEnable(GL_TEXTURE_2D)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glEnableClientState(GL_TEXTURE_COORD_ARRAY)
        glEnableClientState (GL_VERTEX_ARRAY)

    def load_assets_from(self, other_renderer):
        self.texture_manager = other_renderer.texture_manager

    def render(self):
        try:
            self.vbo_manager.run()
        except RuntimeError:
            pass

        glClear (GL_COLOR_BUFFER_BIT)

        glEnable(GL_TEXTURE_2D)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glBindBuffer (GL_ARRAY_BUFFER, self.vbo)
        glVertexPointer (3, GL_FLOAT, 0, None)
        glBindBuffer(GL_ARRAY_BUFFER, self.vbo_1)
        glTexCoordPointer(2, GL_FLOAT, 0, None)

        glDrawArrays (GL_QUADS, 0, len(self.vertices))
        glDisable(GL_TEXTURE_2D)
        glDisable(GL_BLEND)

    def add(self, posList, texCoords):
        self.to_add.append((numpy.array(posList), numpy.array(texCoords)))

    def update_vbo(self):
        pass

world.py

from terrain import *
from player import *
from core.renderer import *
import threading
import random
import glfw

def execute_with_delay(func, delay):
    threading.Timer(delay, func).start()

class ThreadedChunkGenerator():
    def __init__(self, world):
        self.thread = threading.Thread(target=self.run, daemon=True)
        self.world = world
        self.event = threading.Event()
        self.event.wait()
        glfw.make_context_current(self.world.parent.window)

    def run(self,):
        glfw.window_hint(glfw.VISIBLE, glfw.FALSE)
        window2 = glfw.create_window(300, 300, "Window 2", None, self.world.parent.window)
        glfw.make_context_current(window2)
        self.event.set()

        renderer = TerrainRenderer(window2)
        renderer.load_assets_from(self.world.parent)
        while True:
            for i in range(self.world.to_generate):
                chunk = i
                renderer = self.world.parent

                chunk.generate(renderer)
                self.world.renderer = renderer

            self.world.to_generate = []

class World:
    def __init__(self, renderer, player):
        self.parent = renderer
        self.chunks = {}
        self.blocks = {}
        self.position = (0 * 16, 0 * 16)
        self.render_distance = 1
        self.infgen_threshold = 1
        self.block_types = all_blocks(renderer)
        self.to_generate = []
        self.player = player

        self.thread = ThreadedChunkGenerator(self)
        self.thread.start()
        self.event = self.thread.event
        self.event.wait()

    def block_exists(self, position):
        return position in self.blocks

    def _add_chunk(self, position):
        self.chunks[position] = Chunk(self.parent, self, position)

    def add_chunk(self, position):
        execute_with_delay(lambda: self._add_chunk(position), random.randrange(1, 2))

    def generate(self):
        for i in range(self.position[0] - self.render_distance, self.position[0] + self.render_distance + 1):
            for j in range(self.position[1] - self.render_distance, self.position[1] + self.render_distance + 1):
                if (i, j) not in self.chunks:
                    self.add_chunk((i, j))

    def update_infgen(self, position):
        player_pos = (position[0] // 16, position[2] // 16)

        if player_pos[0] - self.position[0] > self.infgen_threshold:
            self.position = (self.position[0] + 2, self.position[1])
            self.generate()
        elif player_pos[0] - self.position[0] < -self.infgen_threshold:
            self.position = (self.position[0] - 2, self.position[1])
            self.generate()
        if player_pos[1] - self.position[1] > self.infgen_threshold:
            self.position = (self.position[0], self.position[1] + 2)
            self.generate()
        elif player_pos[1] - self.position[1] < -self.infgen_threshold:
            self.position = (self.position[0], self.position[1] - 2)
            self.generate()

    def render(self):
        self.parent.render()
        self.update_infgen(self.player.pos)
                    

Right now, it shows no errors, just hangs the window before it even starts rendering. Any help will be highly appreciated.

\$\endgroup\$
2
  • \$\begingroup\$ AFAIK, you can't update OpenGL from multiple threads, everything has to be done from the main thread. Maybe you could update the content in a thread, then sync your threads and update the VBO, but I'm not sure you'll gain much. Why do you have two rendering contexts? \$\endgroup\$
    – Vaillancourt
    Commented Jun 13, 2022 at 16:40
  • \$\begingroup\$ I have posted an updated version of this question to StackOverflow: stackoverflow.com/questions/73192775/… \$\endgroup\$
    – N3RDIUM
    Commented Aug 4, 2022 at 12:55

1 Answer 1

3
+50
\$\begingroup\$

If you want to be able to read and write buffers while they're being used, you can use "persistently-mapped buffers". Be aware that by using them, you will become responsible for handling all synchronization of your buffer data, which will substantially complicate your renderer. Thus, I don't actually suggest trying this, especially if you are new to graphics programming.

Before you try any more advanced approaches, I recommend keeping a host-local copy of your data (i.e. in normal CPU memory) and write into that with your thread, and then use your main OpenGL thread to copy it into the buffer when it's time. OpenGL is fundamentally single-threaded, trying to interact with it from multiple threads will give no performance improvement at best, and may even harm performance.

Finally, I feel obligated to point out that due to Python's global interpreter lock (GIL), threads from python's threading library cannot execute in parallel. If you want true hardware parallelism in a python program, you'll need to either write a C/C++ extension for python (i.e. your own python module), or use the multiprocessing library to avoid the GIL.

\$\endgroup\$
3
  • \$\begingroup\$ And how do I overcome the error: cannot pickle ctypes pointers on multiprocessing? \$\endgroup\$
    – N3RDIUM
    Commented Jul 31, 2022 at 13:23
  • 1
    \$\begingroup\$ I'm not sure, I haven't used multiprocessing together with ctypes and I don't typically use Python for these sorts of applications. Since you can't actually use multiprocessing with OpenGL directly, I would say that question is better answered elsewhere, on a more general python-specific channel. \$\endgroup\$ Commented Aug 1, 2022 at 23:26
  • \$\begingroup\$ Finally got it working. Thanks for the help! \$\endgroup\$
    – N3RDIUM
    Commented Jul 12, 2023 at 9:09

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .