Source code for world

"""
Global 3D environment and multi-threading architecture controller.

The World manager orchestrates the engine's asynchronous lifeblood: dynamic chunk
streaming, ThreadPool dispatching for mesh generation and lighting, and background
Write-Ahead Logging (WAL) SQLite disk saving/loading. It directly handles the
GPU dispatch via hardware occlusion queries and dynamic vectorized frustum culling.
"""

import concurrent.futures
import datetime
import json
import os
import sqlite3
import threading
import time
import zlib
from collections import deque
from typing import Any, Dict, List, Optional, Tuple

import moderngl as mgl
import numpy as np
from numpy.typing import NDArray
from pyglm import glm

import noise
from frustum import frustum_cull_fast
from lighting import (
    init_chunk_lighting,
    place_torch,
    stitch_chunk_lighting,
    update_light_place_block,
    update_light_remove_block,
)
from meshes.chunk_mesh_builder import build_chunk_mesh
from meshes.cube_mesh import CubeMesh
from profiler import global_profiler
from settings import (
    CHUNK_SIZE,
    CHUNK_VOLUME,
    MAIN_THREAD_CHUNK_PROCESS_LIMIT_INGAME,
    MAIN_THREAD_CHUNK_PROCESS_LIMIT_LOADING,
    MAIN_THREAD_MESH_PROCESS_LIMIT_INGAME,
    MAIN_THREAD_MESH_PROCESS_LIMIT_LOADING,
    MESH_BUILD_LIMIT_INGAME,
    MESH_BUILD_LIMIT_LOADING,
    PLAYER_EYE_HEIGHT,
    VBO_POOL_CAP,
    WORLD_AREA,
    WORLD_DEPTH,
    WORLD_HEIGHT,
    WORLD_VOLUME,
    WORLD_WIDTH,
)
from voxel_handler import VoxelHandler
from world_objects.chunk import Chunk


[docs] class World: """ Manages the global 3D voxel environment. Responsible for chunk streaming, multithreaded terrain generation, mesh building queues, and persistent background SQLite disk storage. It acts as the central hub linking the player, terrain data, lighting updates, and GPU render dispatching. Args: app (Any): The main Pyrite application instance. save_name (str): The string identifier/filename for the SQLite world database. world_seed (int): The deterministic seed used for procedural terrain generation. """ @global_profiler.profile_func('World_Init') def __init__(self, app: Any, save_name: str, world_seed: int) -> None: """ Initializes the world arrays, establishes an async-like SQLite database connection, restores player data, and warms up the Numba compiler. """ self.world_seed: int = world_seed self.app: Any = app self.app.render_loading_screen('ALLOCATING MEMORY...') self.chunks: List[Optional[Chunk]] = [None for _ in range(WORLD_VOLUME)] self.active_chunks: Dict[Tuple[int, int, int], Chunk] = {} self.chunk_positions: NDArray[np.int32] = np.full((WORLD_VOLUME, 3), -999, dtype='int32') self.executor: concurrent.futures.ThreadPoolExecutor = concurrent.futures.ThreadPoolExecutor( max_workers=max(4, (os.cpu_count() or 5) - 1) ) self.mesh_queue: List[Tuple[Optional[Chunk], concurrent.futures.Future[Any]]] = [] self.build_queue: List[Chunk] = [] self.load_queue: List[Tuple[Optional[Chunk], concurrent.futures.Future[Any]]] = [] self.voxels: NDArray[np.uint8] = np.empty([WORLD_VOLUME, CHUNK_VOLUME], dtype='uint8') self.lightmaps: NDArray[np.uint8] = np.full([WORLD_VOLUME, CHUNK_VOLUME], 255, dtype='uint8') self.voxel_handler: Any = VoxelHandler(self) self.vbo_pool: deque[Tuple[Any, Any]] = deque() self.last_player_chunk_pos: Optional[Tuple[int, int]] = None self.sorted_chunks: List[Chunk] = [] self.last_active_chunk_count: int = 0 self.chunk_centers: NDArray[np.float32] = np.empty((0, 3), dtype='float32') self.frustum_mask: NDArray[np.bool_] = np.empty(0, dtype=np.bool_) self.bbox_mesh: Any = CubeMesh(app) self.app.render_loading_screen('CONNECTING TO DATABASE...') self.save_name: str = save_name os.makedirs('saves', exist_ok=True) self.save_path: str = f'saves/{self.save_name}.db' self.thread_local: threading.local = threading.local() self.thread_connections: List[sqlite3.Connection] = [] self.thread_cursors: List[sqlite3.Cursor] = [] self.db_lock: threading.Lock = threading.Lock() self.connection: sqlite3.Connection = sqlite3.connect(self.save_path, check_same_thread=False) self.cursor: sqlite3.Cursor = self.connection.cursor() self.cursor.execute("""CREATE TABLE IF NOT EXISTS chunks ( x INTEGER, y INTEGER, z INTEGER, data BLOB, PRIMARY KEY (x, y, z))""") self.cursor.execute("""CREATE TABLE IF NOT EXISTS player_data ( id INTEGER PRIMARY KEY, data TEXT)""") self.cursor.execute("""CREATE TABLE IF NOT EXISTS world_meta ( id INTEGER PRIMARY KEY, world_name TEXT, seed INTEGER, game_mode INTEGER, creation_date TEXT, last_played TEXT)""") self.cursor.execute("""CREATE TABLE IF NOT EXISTS dropped_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, voxel_id INTEGER, position_x REAL, position_y REAL, position_z REAL, velocity_x REAL, velocity_y REAL, velocity_z REAL)""") # Safely upgrade existing databases to support lightmap caching! try: self.cursor.execute('ALTER TABLE chunks ADD COLUMN lightmap BLOB') except sqlite3.OperationalError: pass # Column already exists! self.connection.commit() # Optimize SQLite for async-like high performance disk writing self.cursor.execute('PRAGMA journal_mode = WAL') self.cursor.execute('PRAGMA synchronous = NORMAL') # Initialize metadata if missing (e.g. creating a new world) self.cursor.execute('SELECT * FROM world_meta WHERE id=1') meta_row = self.cursor.fetchone() if not meta_row: now = datetime.datetime.now().isoformat() # Use the seed passed from main.position_y self.cursor.execute( """INSERT INTO world_meta (id, world_name, seed, game_mode, creation_date, last_played) VALUES (?, ?, ?, ?, ?, ?)""", (1, self.save_name.replace('_', ' '), world_seed, self.app.player.game_mode, now, now), ) self.connection.commit() else: self.app.player.game_mode = meta_row[3] # Load player inventory and hotbar state self.cursor.execute('SELECT data FROM player_data WHERE id=1') row = self.cursor.fetchone() if row: try: p_data = json.loads(row[0]) loaded_inv = p_data.get('inventory', []) loaded_counts = p_data.get('counts', []) # Safely copy items over up to the current INVENTORY_SIZE to prevent crashes from old saves for i in range(min(len(loaded_inv), len(self.app.player.inventory))): self.app.player.inventory[i] = loaded_inv[i] self.app.player.inventory_counts[i] = loaded_counts[i] self.app.player.hotbar_index = p_data.get('hotbar_index', self.app.player.hotbar_index) self.app.player.health = p_data.get('health', self.app.player.max_health) self.app.player.hunger = p_data.get('hunger', self.app.player.max_hunger) self.app.player.oxygen = p_data.get('oxygen', self.app.player.max_oxygen) self.app.world_session_time = p_data.get('time_played', 0.0) position = p_data.get('position') if position: self.app.player.position = glm.vec3(position[0], position[1], position[2]) self.app.player.feet_pos = glm.vec3(position[0], position[1] - PLAYER_EYE_HEIGHT, position[2]) self.app.player.highest_y = position[1] else: self.app.player.respawn() yaw = p_data.get('yaw') if yaw is not None: self.app.player.yaw = yaw pitch = p_data.get('pitch') if pitch is not None: self.app.player.pitch = pitch except Exception as e: print(f'[SYSTEM] Failed to load player data: {e}') self.app.player.respawn() else: self.app.player.respawn() # Load dropped items self.saved_dropped_items: List[Any] = [] try: self.cursor.execute( 'SELECT voxel_id, position_x, position_y, position_z, velocity_x, velocity_y, velocity_z FROM dropped_items' ) self.saved_dropped_items = self.cursor.fetchall() except sqlite3.OperationalError: pass # Table might not exist in old saves before migration # WARM UP NUMBA COMPILER: # Run Numba compilation on a background thread so the main thread can keep pumping Pygame events! self.app.render_loading_screen('COMPILING NUMBA JIT (MAY TAKE A MOMENT)...') print('[SYSTEM] Warming up Numba JIT Compiler... this may take a few seconds.') t0 = time.perf_counter() def compile_numba() -> None: dummy_voxels = np.zeros(CHUNK_VOLUME, dtype='uint8') dummy_lights = np.full(CHUNK_VOLUME, 255, dtype='uint8') Chunk.generate_terrain( dummy_voxels, dummy_lights, 0, 0, 0, noise.perm, noise.perm_grad_index3, self.world_seed ) init_chunk_lighting(0, 0, 0, self.voxels, self.lightmaps, self.chunk_positions) stitch_chunk_lighting(0, 0, 0, self.voxels, self.lightmaps, self.chunk_positions) update_light_place_block(0, 0, 0, self.voxels, self.lightmaps, self.chunk_positions) update_light_remove_block(0, 0, 0, self.voxels, self.lightmaps, self.chunk_positions) place_torch(0, 0, 0, self.voxels, self.lightmaps, self.chunk_positions) build_chunk_mesh( chunk_voxels=dummy_voxels, chunk_lightmap=dummy_lights, format_size=2, chunk_pos=(0, 0, 0), world_voxels=self.voxels, world_lightmaps=self.lightmaps, chunk_positions=self.chunk_positions, ) with global_profiler.measure('Numba_Warmup_Submit'): future = self.executor.submit(compile_numba) while not future.done(): self.app.render_loading_screen('COMPILING NUMBA JIT (MAY TAKE A MOMENT)...') self.app.clock.tick(60) print(f'[SYSTEM] Numba compilation finished in {time.perf_counter() - t0:.3f} seconds!') self.app.render_loading_screen('NUMBA COMPILATION SUCCESSFUL!')
[docs] @global_profiler.profile_func('World_Update') def update(self) -> None: """ Tick loop that handles continuous background data processing. Steps through loading chunks, dispatching thread-pool mesh tasks, and streaming logic. """ self.db_load_time: float = 0.0 self.terrain_gen_time: float = 0.0 self.voxel_handler.update() self.stream_chunks() self.process_load_queue() if self.db_load_time > 0 or self.terrain_gen_time > 0: print( f'[FRAME TIME] Chunk Loading -> DB Read: {self.db_load_time:.4f}s | Terrain Gen: {self.terrain_gen_time:.4f}s' ) # Sort the build queue so the closest chunks are popped from the end first if self.build_queue: player_cx = int(self.app.player.position.x // CHUNK_SIZE) player_cz = int(self.app.player.position.z // CHUNK_SIZE) # Optimization: Only re-sort if the player moves to a new chunk or the queue size changes! if getattr(self, '_last_sort_pos', None) != (player_cx, player_cz) or getattr( self, '_last_queue_len', None ) != len(self.build_queue): self.build_queue.sort( key=lambda c: (c.position[0] - player_cx) ** 2 + (c.position[2] - player_cz) ** 2, reverse=True ) self._last_sort_pos = (player_cx, player_cz) self._last_queue_len = len(self.build_queue) # Submit tasks gradually to prevent ThreadPool starvation and startup lag mesh_limit = MESH_BUILD_LIMIT_INGAME if self.app.game_state != 'LOADING' else MESH_BUILD_LIMIT_LOADING while self.build_queue and len(self.mesh_queue) < mesh_limit: chunk = self.build_queue.pop() nl = getattr(chunk, 'pending_lighting', False) chunk.pending_lighting = False def build_task(c: Any = chunk, needs_light: bool = nl) -> Any: chunk_x, chunk_y, chunk_z = c.position if needs_light: init_chunk_lighting( chunk_x * CHUNK_SIZE, chunk_y * CHUNK_SIZE, chunk_z * CHUNK_SIZE, self.voxels, self.lightmaps, self.chunk_positions, ) stitch_chunk_lighting( chunk_x * CHUNK_SIZE, chunk_y * CHUNK_SIZE, chunk_z * CHUNK_SIZE, self.voxels, self.lightmaps, self.chunk_positions, ) return c.mesh.get_vertex_data() future = self.executor.submit(build_task) self.mesh_queue.append((chunk, future)) self.process_mesh_queue()
# OpenGL Thread-Safety & VBO Pooling # In this engine, meshing (calculating the 3D geometry of chunks) happens on # background threads. However, there is a strict rule in graphics programming: # **You cannot communicate with the GPU (OpenGL) outside of the Main Thread.** # # How we solve this: # 1. Background threads calculate the raw vertex arrays (`vertex_data`). # 2. They pass this array to the `mesh_queue`. # 3. This function (`process_mesh_queue`) runs strictly on the Main Thread. # It grabs the data and safely creates the OpenGL Vertex Buffer Objects (VBOs). # # The VBO Recycling Pool: # Creating and destroying VBOs constantly as chunks load/unload causes massive # VRAM fragmentation and GC stutters. Instead, when a chunk unloads, we throw # its VBO pointer into a `vbo_pool`. When a new chunk needs a VBO, it simply # pulls an old one from the pool and overwrites it. Memory stays perfectly flat! # # References: # - OpenGL Thread Safety: https://www.khronos.org/opengl/wiki/OpenGL_and_multithreading # - Object Pooling Pattern: https://gameprogrammingpatterns.com/object-pool.html
[docs] @global_profiler.profile_func('Process_Mesh_Queue') def process_mesh_queue(self) -> None: """ Pulls completed mesh data from background threads and safely initializes OpenGL Vertex Array Objects (VAOs) on the main thread, utilizing a recycling VBO pool to prevent VRAM memory leaks. """ # Safely create OpenGL VAOs on the main thread ready_count = 0 for item in list(self.mesh_queue): chunk, future = item if chunk is None: continue if future.done(): result = future.result() if chunk.mesh: chunk.mesh.vertex_data = result[0] chunk.mesh.opaque_count = result[1] chunk.mesh.water_count = result[2] # Recycle the old VBO/VAO to prevent memory leaks during chunk remeshing if chunk.mesh.vao and chunk.mesh.vbo: self.vbo_pool.append((chunk.mesh.vbo, chunk.mesh.vao)) while len(self.vbo_pool) > VBO_POOL_CAP: p_vbo, p_vao = self.vbo_pool.popleft() p_vbo.release() p_vao.release() chunk.mesh.vao = chunk.mesh.get_vao() self.mesh_queue.remove(item) ready_count += 1 limit = ( MAIN_THREAD_MESH_PROCESS_LIMIT_LOADING if self.app.game_state == 'LOADING' else MAIN_THREAD_MESH_PROCESS_LIMIT_INGAME ) if ready_count >= limit: # Limit processing to prevent frame drops break
[docs] @global_profiler.profile_func('Process_Load_Queue') def process_load_queue(self) -> None: """ Consumes asynchronously loaded/generated chunk data, registers it into the active world arrays, applies volumetric lighting (BFS), and schedules the chunk and its neighbors for mesh building. """ processed = 0 for item in list(self.load_queue): chunk, future = item if chunk is None: continue if future.done(): source, elapsed_time, voxel_data, lightmap_data, is_empty, needs_lighting = future.result() if source == 'db': self.db_load_time += elapsed_time else: self.terrain_gen_time += elapsed_time # Only apply if the chunk wasn't unloaded while loading if self.active_chunks.get(chunk.position) is chunk: chunk_index = ( (chunk.position[0] % WORLD_WIDTH) + WORLD_WIDTH * (chunk.position[2] % WORLD_DEPTH) + WORLD_AREA * (chunk.position[1] % WORLD_HEIGHT) ) x, y, z = chunk.position self.voxels[chunk_index] = voxel_data chunk.voxels = self.voxels[chunk_index] self.lightmaps[chunk_index] = lightmap_data chunk.lightmap = self.lightmaps[chunk_index] chunk.is_empty = is_empty self.chunk_positions[chunk_index] = chunk.position if needs_lighting: chunk.pending_lighting = True chunk.build_mesh() self.build_queue.append(chunk) # Force remesh of all 6 chunk neighbors to ensure light spills render properly for dx, dy, dz in [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]: n_pos = (x + dx, y + dy, z + dz) if n_pos in self.active_chunks: n_chunk = self.active_chunks[n_pos] if n_chunk.voxels is not None and n_chunk not in self.build_queue: self.build_queue.append(n_chunk) self.load_queue.remove(item) processed += 1 # Limit chunks processed per frame to prevent FPS drops and Main Thread freezing! limit = ( MAIN_THREAD_CHUNK_PROCESS_LIMIT_LOADING if self.app.game_state == 'LOADING' else MAIN_THREAD_CHUNK_PROCESS_LIMIT_INGAME ) if processed >= limit: break
@global_profiler.profile_func('Fetch_Or_Generate_Voxels') def _fetch_or_generate_voxels( self, x: int, y: int, z: int ) -> Tuple[str, float, NDArray[np.uint8], NDArray[np.uint8], bool, bool]: """ Background worker function that attempts to retrieve compressed chunk data from the SQLite database. If the chunk has never been visited, generates brand new procedural terrain using Numba logic instead. """ t0 = time.perf_counter() chunk_x, chunk_y, chunk_z = x * CHUNK_SIZE, y * CHUNK_SIZE, z * CHUNK_SIZE if not hasattr(self.thread_local, 'cursor'): conn = sqlite3.connect(self.save_path, timeout=10, check_same_thread=False) self.thread_local.cursor = conn.cursor() with self.db_lock: self.thread_connections.append(conn) self.thread_cursors.append(self.thread_local.cursor) self.thread_local.cursor.execute('SELECT data, lightmap FROM chunks WHERE x=? AND y=? AND z=?', (x, y, z)) row = self.thread_local.cursor.fetchone() if row: voxel_data = np.frombuffer(zlib.decompress(row[0]), dtype='uint8').copy() is_empty = not np.any(voxel_data) if len(row) > 1 and row[1] is not None: # Lightmap exists in DB! Skip expensive Numba BFS calculations entirely! lightmap_data = np.frombuffer(zlib.decompress(row[1]), dtype='uint8').copy() return ('db', time.perf_counter() - t0, voxel_data, lightmap_data, is_empty, False) # Old save file format, fallback to generating sunlight lightmap_data = np.zeros(CHUNK_VOLUME, dtype='uint8') Chunk.fill_initial_sunlight_only(voxel_data, lightmap_data, chunk_x, chunk_y, chunk_z, noise.perm) return ('db', time.perf_counter() - t0, voxel_data, lightmap_data, is_empty, True) voxel_data = np.zeros(CHUNK_VOLUME, dtype='uint8') lightmap_data = np.zeros(CHUNK_VOLUME, dtype='uint8') Chunk.generate_terrain( voxel_data, lightmap_data, chunk_x, chunk_y, chunk_z, noise.perm, noise.perm_grad_index3, self.world_seed ) is_empty = not np.any(voxel_data) return ('gen', time.perf_counter() - t0, voxel_data, lightmap_data, is_empty, True)
[docs] @global_profiler.profile_func('Stream_Chunks') def stream_chunks(self) -> None: """ Checks the player's position against the render distance to determine which distant chunks to unload, and which new surrounding chunks to queue for background loading. """ render_dist = int(self.app.config.get('render_distance', 4)) player_cx = int(self.app.player.position.x // CHUNK_SIZE) player_cz = int(self.app.player.position.z // CHUNK_SIZE) # Optimization: Only scan for chunks to stream if the player moves to a new chunk or changes render distance! stream_state = (player_cx, player_cz, render_dist) if getattr(self, 'last_stream_state', None) == stream_state: return self.last_stream_state = stream_state # 1. Unload chunks out of range for position in list(self.active_chunks.keys()): x, y, z = position if (x - player_cx) ** 2 + (z - player_cz) ** 2 > (render_dist + 1) ** 2: self.unload_chunk(position) # 2. Load chunks in range chunks_to_load = [] for x in range(player_cx - render_dist, player_cx + render_dist + 1): for z in range(player_cz - render_dist, player_cz + render_dist + 1): if (x - player_cx) ** 2 + (z - player_cz) ** 2 > render_dist**2: continue for y in range(WORLD_HEIGHT): if (x, y, z) not in self.active_chunks: self.load_chunk(x, y, z) chunks_to_load.append((x, y, z)) # Sort chunks by distance (closest first) so the ThreadPool executes them first chunks_to_load.sort(key=lambda position: (position[0] - player_cx) ** 2 + (position[2] - player_cz) ** 2) for position in chunks_to_load: self.load_chunk(*position)
[docs] @global_profiler.profile_func('Load_Chunk') def load_chunk(self, x: int, y: int, z: int) -> None: """ Initializes a Chunk instance at the given coordinates and dispatches an asynchronous task to fetch or generate its actual voxel data. """ # Register chunk chunk_index = (x % WORLD_WIDTH) + WORLD_WIDTH * (z % WORLD_DEPTH) + WORLD_AREA * (y % WORLD_HEIGHT) old_chunk = self.chunks[chunk_index] if old_chunk: self.unload_chunk(old_chunk.position) # Create chunk chunk = Chunk(self, position=(x, y, z)) self.chunks[chunk_index] = chunk self.active_chunks[(x, y, z)] = chunk # Dispatch load task future = self.executor.submit(self._fetch_or_generate_voxels, x, y, z) self.load_queue.append((chunk, future))
[docs] @global_profiler.profile_func('Save_Chunk_To_DB') def save_chunk_to_db( self, x: int, y: int, z: int, voxels: NDArray[np.uint8], lightmap: Optional[NDArray[np.uint8]] ) -> None: """ Compresses a chunk's massive 1D voxel and lighting arrays using zlib, and executes a thread-safe write to the SQLite database. """ data = zlib.compress(voxels.tobytes()) l_data = zlib.compress(lightmap.tobytes()) if lightmap is not None else None with self.db_lock: self.cursor.execute( 'INSERT OR REPLACE INTO chunks (x, y, z, data, lightmap) VALUES (?, ?, ?, ?, ?)', (x, y, z, data, l_data), ) self.connection.commit()
[docs] @global_profiler.profile_func('Unload_Chunk') def unload_chunk(self, position: Tuple[int, int, int]) -> None: """ Removes a chunk from the active world space, triggers an asynchronous disk save, purges it from any pending queues, and recycles its VRAM. """ if position in self.active_chunks: chunk = self.active_chunks.pop(position) if not chunk.is_empty and chunk.voxels is not None: lightmap_copy = chunk.lightmap.copy() if chunk.lightmap is not None else None self.executor.submit( self.save_chunk_to_db, position[0], position[1], position[2], chunk.voxels.copy(), lightmap_copy ) chunk_index = ( (position[0] % WORLD_WIDTH) + WORLD_WIDTH * (position[2] % WORLD_DEPTH) + WORLD_AREA * (position[1] % WORLD_HEIGHT) ) self.chunks[chunk_index] = None self.chunk_positions[chunk_index] = (-999, -999, -999) if chunk.mesh: if chunk.mesh.vao and chunk.mesh.vbo: self.vbo_pool.append((chunk.mesh.vbo, chunk.mesh.vao)) while len(self.vbo_pool) > VBO_POOL_CAP: p_vbo, p_vao = self.vbo_pool.popleft() p_vbo.release() p_vao.release() chunk.mesh.vbo, chunk.mesh.vao = None, None if chunk in self.build_queue: self.build_queue.remove(chunk) for item in list(self.load_queue): if item[0] is chunk: self.load_queue.remove(item) break for item in list(self.mesh_queue): if item[0] is chunk: self.mesh_queue.remove(item) break
[docs] @global_profiler.profile_func('World_Render') def render(self) -> None: """ Performs dynamic vectorized frustum culling and hardware occlusion queries to identify visible chunks, then safely dispatches rendering calls to the GPU. """ player = self.app.player player_position = player.position player_chunk_pos = (int(player_position.x // CHUNK_SIZE), int(player_position.z // CHUNK_SIZE)) active_chunk_count = len(self.active_chunks) # Re-sort chunks only when player moves to a new chunk or when chunks are loaded/unloaded if player_chunk_pos != self.last_player_chunk_pos or active_chunk_count != self.last_active_chunk_count: self.sorted_chunks = sorted( self.active_chunks.values(), key=lambda c: glm.distance2(c.center, player_position) ) # Update the chunk centers array for vectorized culling if self.sorted_chunks: self.chunk_centers = np.array([c.center for c in self.sorted_chunks], dtype='float32') self.frustum_mask = np.ones(len(self.sorted_chunks), dtype=np.bool_) else: self.chunk_centers = np.empty((0, 3), dtype='float32') self.frustum_mask = np.ones(0, dtype=np.bool_) self.last_player_chunk_pos = player_chunk_pos self.last_active_chunk_count = active_chunk_count freeze = getattr(self.app, 'freeze_culling', False) # Vectorized frustum culling if not freeze and len(self.chunk_centers) > 0: frustum = player.frustum with global_profiler.measure('Frustum_Culling'): frustum_cull_fast( self.chunk_centers, self.frustum_mask, np.array(player_position, dtype='float32'), np.array(player.forward, dtype='float32'), np.array(player.right, dtype='float32'), np.array(player.up, dtype='float32'), frustum.tangent_y, frustum.tangent_x, frustum.factor_y, frustum.factor_x, ) # 1. Update visibility from occlusion queries if not freeze: for i, chunk in enumerate(self.sorted_chunks): if not self.frustum_mask[i] or chunk.is_empty: chunk.is_visible = False continue if chunk.query_submitted: chunk.is_visible = chunk.query.samples > 0 else: chunk.is_visible = True # Assume visible until queried # 2. Render visible chunks AND query them simultaneously for chunk in self.sorted_chunks: if chunk.is_visible: if not freeze: with chunk.query: chunk.render() chunk.query_submitted = True else: chunk.render() # 3. Issue occlusion queries for INVISIBLE chunks if not freeze: ctx = self.app.ctx fbo = getattr(ctx, 'fbo', getattr(ctx, 'screen', getattr(ctx, 'default_framebuffer', None))) if fbo: fbo.color_mask = (False, False, False, False) fbo.depth_mask = False ctx.depth_func = '<=' ctx.disable(mgl.CULL_FACE) bbox_prog = self.app.shader_program.voxel_marker bbox_vao = self.bbox_mesh.vao bbox_prog['is_bbox'] = 1 for i, chunk in enumerate(self.sorted_chunks): if not chunk.is_visible: # Only query chunks that are IN the frustum but currently occluded if chunk.is_empty or not self.frustum_mask[i]: chunk.query_submitted = False continue m_model = glm.translate(glm.mat4(), glm.vec3(chunk.position) * CHUNK_SIZE) m_model = glm.scale(m_model, glm.vec3(CHUNK_SIZE)) bbox_prog['m_model'].write(m_model) with chunk.query: bbox_vao.render() chunk.query_submitted = True bbox_prog['is_bbox'] = 0 if fbo: fbo.color_mask = (True, True, True, True) fbo.depth_mask = True ctx.depth_func = '<' ctx.enable(mgl.CULL_FACE)
[docs] @global_profiler.profile_func('World_Render_Water') def render_water(self) -> None: """ A secondary rendering pass explicitly designed to draw transparent water meshes properly blended over the previously drawn opaque terrain. """ # We assume visibility is already calculated by the primary render() pass! for chunk in self.sorted_chunks: if chunk.is_visible: chunk.render_water()
[docs] @global_profiler.profile_func('World_Save') def save(self) -> None: """ Synchronously dumps all currently active chunks, inventory contents, player coordinates, and world metadata safely to the SQLite disk on exit. """ try: # Save active chunks synchronously for chunk in self.active_chunks.values(): if not chunk.is_empty and chunk.voxels is not None: self.save_chunk_to_db( chunk.position[0], chunk.position[1], chunk.position[2], chunk.voxels, chunk.lightmap ) # Save player state and inventory p_data = { 'inventory': [int(item) for item in self.app.player.inventory], 'counts': [int(count) for count in self.app.player.inventory_counts], 'hotbar_index': int(self.app.player.hotbar_index), 'position': [ float(self.app.player.position.x), float(self.app.player.position.y), float(self.app.player.position.z), ], 'yaw': float(self.app.player.yaw), 'pitch': float(self.app.player.pitch), 'health': float(self.app.player.health), 'hunger': float(self.app.player.hunger), 'oxygen': float(self.app.player.oxygen), 'time_played': float(self.app.world_session_time), } now = datetime.datetime.now().isoformat() with self.db_lock: # Update database records self.cursor.execute( 'UPDATE world_meta SET last_played = ?, game_mode = ? WHERE id=1', (now, self.app.player.game_mode) ) self.cursor.execute( 'INSERT OR REPLACE INTO player_data (id, data) VALUES (?, ?)', (1, json.dumps(p_data)) ) # Save dropped items if self.app.scene and hasattr(self.app.scene, 'item_manager'): self.cursor.execute('DELETE FROM dropped_items') item_data = [] for item in self.app.scene.item_manager.items: item_data.append( ( int(item.voxel_id), float(item.position.x), float(item.position.y), float(item.position.z), float(item.velocity.x), float(item.velocity.y), float(item.velocity.z), ) ) self.cursor.executemany( 'INSERT INTO dropped_items (voxel_id, position_x, position_y, position_z, velocity_x, velocity_y, velocity_z) VALUES (?, ?, ?, ?, ?, ?, ?)', item_data, ) self.connection.commit() except Exception as e: print(f'[SYSTEM] Error during World.save(): {e}') finally: # Shutdown executor and threads self.executor.shutdown(wait=True) for cur in self.thread_cursors: try: cur.close() except Exception as e: print(f'[SYSTEM] Error closing background cursor: {e}') self.thread_cursors.clear() for conn in self.thread_connections: try: conn.close() except Exception as e: print(f'[SYSTEM] Error closing background connection: {e}') self.thread_connections.clear() try: self.cursor.close() except Exception as e: print(f'[SYSTEM] Error closing cursor: {e}') try: self.connection.close() except Exception as e: print(f'[SYSTEM] Error closing connection: {e}') # Release OpenGL resources for vbo, vao in self.vbo_pool: try: vbo.release() vao.release() except Exception as e: print(f'[SYSTEM] Error releasing VBO/VAO from pool: {e}') self.vbo_pool.clear() for ch in self.chunks: if ch: if ch.mesh: if ch.mesh.vao: try: ch.mesh.vao.release() except Exception as e: print(f'[SYSTEM] Error releasing chunk VAO: {e}') if ch.mesh.vbo: try: ch.mesh.vbo.release() except Exception as e: print(f'[SYSTEM] Error releasing chunk VBO: {e}') if self.bbox_mesh and self.bbox_mesh.vao: try: self.bbox_mesh.vao.release() except Exception as e: print(f'[SYSTEM] Error releasing bbox VAO: {e}')