diff --git a/Krom/Krom.exe b/Krom/Krom.exe index ffba9d3f..7b3ca1e0 100644 Binary files a/Krom/Krom.exe and b/Krom/Krom.exe differ diff --git a/leenkx/blender/lnx/handlers.py b/leenkx/blender/lnx/handlers.py index 295f657f..03b8f643 100644 --- a/leenkx/blender/lnx/handlers.py +++ b/leenkx/blender/lnx/handlers.py @@ -136,7 +136,10 @@ def always() -> float: # Force ui redraw if state.redraw_ui: if bpy.context.screen is not None: + krom_active = bool(lnx.render_engine._active_krom_engines) for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D' and krom_active: + continue if area.type in ('NODE_EDITOR', 'PROPERTIES', 'VIEW_3D'): area.tag_redraw() state.redraw_ui = False diff --git a/leenkx/blender/lnx/make.py b/leenkx/blender/lnx/make.py index f780d3cb..59379146 100644 --- a/leenkx/blender/lnx/make.py +++ b/leenkx/blender/lnx/make.py @@ -804,41 +804,71 @@ def viewport_build_done(): _viewport_pending_launches.clear() log.error('Viewport build failed, check console') -def play_viewport(viewport_id, width=1920, height=1080): - """Launch Krom in a viewport""" - global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build +def play(viewport_id=None, width=1920, height=1080): + """Launch player external or embedded when viewport_id is passed""" + global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build, scripts_mtime - if not viewport_id: - log.error('No viewport_id: play_viewport requires an id') - return - - if 'Lnx' not in bpy.data.worlds: + if viewport_id and 'Lnx' not in bpy.data.worlds: log.error('No Lnx world found - cannot start viewport server') return wrd = bpy.data.worlds['Lnx'] + target = 'krom' if viewport_id else runtime_to_target() - krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js' - if os.path.exists(krom_js_path) and not wrd.lnx_recompile: - run_viewport_runtime(viewport_id, width, height) - return + khajs_path = get_khajs_path(target) + if not wrd.lnx_cache_build or \ + not os.path.isfile(khajs_path) or \ + assets.khafile_defs_last != assets.khafile_defs or \ + state.last_target != state.target: + wrd.lnx_recompile = True - pending_entry = (viewport_id, width, height) - if pending_entry not in _viewport_pending_launches: - _viewport_pending_launches.append(pending_entry) + state.last_target = state.target - if _viewport_build_in_progress: - if _viewport_proc_build is not None and _viewport_proc_build.poll() is None: - log.info(f'Build in progress, viewport {viewport_id} will launch when ready') + state.mod_scripts = [] + script_path = lnx.utils.get_fp() + '/Sources/' + lnx.utils.safestr(wrd.lnx_project_package) + if os.path.isdir(script_path): + new_mtime = scripts_mtime + for fn in glob.iglob(os.path.join(script_path, '**', '*.hx'), recursive=True): + mtime = os.path.getmtime(fn) + if scripts_mtime < mtime: + lnx.utils.fetch_script_props(fn) + fn = fn.split('Sources/')[1] + fn = fn[:-3] + fn = fn.replace('/', '.') + state.mod_scripts.append(fn) + wrd.lnx_recompile = True + if new_mtime < mtime: + new_mtime = mtime + scripts_mtime = new_mtime + if len(state.mod_scripts) > 0: + lnx.utils.fetch_trait_props() + + if viewport_id: + krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js' + if os.path.exists(krom_js_path) and not wrd.lnx_recompile: + run_viewport_runtime(viewport_id, width, height) return - else: - _viewport_build_in_progress = False - _viewport_build_in_progress = True + pending_entry = (viewport_id, width, height) + if pending_entry not in _viewport_pending_launches: + _viewport_pending_launches.append(pending_entry) - build(target='krom', is_viewport=True, viewport_width=width, viewport_height=height) + if _viewport_build_in_progress: + if _viewport_proc_build is not None and _viewport_proc_build.poll() is None: + log.info(f'Build in progress, viewport {viewport_id} will launch when ready') + return + else: + _viewport_build_in_progress = False - compile_viewport(assets_only=(not wrd.lnx_recompile)) + _viewport_build_in_progress = True + + assets.invalidate_enabled = False + build(target='krom', is_viewport=True, viewport_width=width, viewport_height=height) + compile_viewport(assets_only=(not wrd.lnx_recompile)) + assets.invalidate_enabled = True + else: + build(target=target, is_play=True) + compile(assets_only=(not wrd.lnx_recompile)) def stop_viewport(viewport_id=None): @@ -905,43 +935,6 @@ def get_khajs_path(target): return lnx.utils.build_dir() + '/debug/krom/krom.js' return lnx.utils.build_dir() + '/debug/html5/kha.js' -def play(): - global scripts_mtime - wrd = bpy.data.worlds['Lnx'] - - build(target=runtime_to_target(), is_play=True) - - khajs_path = get_khajs_path(state.target) - if not wrd.lnx_cache_build or \ - not os.path.isfile(khajs_path) or \ - assets.khafile_defs_last != assets.khafile_defs or \ - state.last_target != state.target: - wrd.lnx_recompile = True - - state.last_target = state.target - - # Trait sources modified - state.mod_scripts = [] - script_path = lnx.utils.get_fp() + '/Sources/' + lnx.utils.safestr(wrd.lnx_project_package) - if os.path.isdir(script_path): - new_mtime = scripts_mtime - for fn in glob.iglob(os.path.join(script_path, '**', '*.hx'), recursive=True): - mtime = os.path.getmtime(fn) - if scripts_mtime < mtime: - lnx.utils.fetch_script_props(fn) # Trait props - fn = fn.split('Sources/')[1] - fn = fn[:-3] #.hx - fn = fn.replace('/', '.') - state.mod_scripts.append(fn) - wrd.lnx_recompile = True - if new_mtime < mtime: - new_mtime = mtime - scripts_mtime = new_mtime - if len(state.mod_scripts) > 0: # Trait props - lnx.utils.fetch_trait_props() - - compile(assets_only=(not wrd.lnx_recompile)) - def build_success(): log.clear() wrd = bpy.data.worlds['Lnx'] diff --git a/leenkx/blender/lnx/render_engine.py b/leenkx/blender/lnx/render_engine.py index 6b36586d..776c4299 100644 --- a/leenkx/blender/lnx/render_engine.py +++ b/leenkx/blender/lnx/render_engine.py @@ -11,12 +11,16 @@ import sys import os import array import atexit +import time from mathutils import Quaternion, Matrix, Vector import lnx.make_state as state HAS_GPU_STATE = bpy.app.version >= (3, 0, 0) -if not HAS_GPU_STATE: +try: import bgl + HAS_BGL = callable(getattr(bgl, 'glGenTextures', None)) +except ImportError: + HAS_BGL = False SHADER_UNIFORM_COLOR = 'UNIFORM_COLOR' if bpy.app.version >= (3, 4, 0) else '2D_UNIFORM_COLOR' SHADER_IMAGE = 'IMAGE' if bpy.app.version >= (3, 4, 0) else '2D_IMAGE' HAS_GPU_TEXTURE = bpy.app.version >= (3, 0, 0) @@ -124,6 +128,8 @@ class KromViewportEngine(bpy.types.RenderEngine): self._last_frame_id = 0 self._width = 0 self._height = 0 + self._tex_width = 0 + self._tex_height = 0 self._initialized = False self._shader = None self._batch = None @@ -140,6 +146,8 @@ class KromViewportEngine(bpy.types.RenderEngine): self._last_region_dims = None self._header_buffer = (ctypes.c_ubyte * VIEWPORT_HEADER_SIZE)() self._engine_id = id(self) + self._viewport_image = None + self._float_buffer = None # print(f"New engine instance created: {self._engine_id}") def _restore_overlay(self, context=None): @@ -247,7 +255,7 @@ class KromViewportEngine(bpy.types.RenderEngine): def deferred_launch(): try: import lnx.make as make - make.play_viewport(viewport_id, width, height) + make.play(viewport_id, width, height) except Exception as e: print(f"Failed to launch viewport: {e}") import traceback @@ -498,7 +506,7 @@ class KromViewportEngine(bpy.types.RenderEngine): """Handle invalid/stale shared memory by cleaning up and allowing re-init.""" self._cleanup_shared_memory() self._initialized = False - if not HAS_GPU_TEXTURE and hasattr(self, '_gl_texture_buf') and self._gl_texture_buf[0] != 0: + if HAS_BGL and hasattr(self, '_gl_texture_buf') and self._gl_texture_buf[0] != 0: try: bgl.glDeleteTextures(1, self._gl_texture_buf) except: @@ -783,21 +791,8 @@ class KromViewportEngine(bpy.types.RenderEngine): if len(pixels) < num_pixels: return - if HAS_GPU_TEXTURE: - if HAS_NUMPY: - pixels[3::4] = 255 - float_array = pixels.astype(np.float32) / 255.0 - buffer = gpu.types.Buffer('FLOAT', num_pixels, float_array) - else: - pixel_array = array.array('B', pixels[:num_pixels]) - for i in range(3, len(pixel_array), 4): - pixel_array[i] = 255 - float_pixels = [p / 255.0 for p in pixel_array] - buffer = gpu.types.Buffer('FLOAT', num_pixels, float_pixels) - - self._texture = gpu.types.GPUTexture((width, height), format='SRGB8_A8', data=buffer) - else: - # bgl path for Blender < 3.0 + if HAS_BGL: + # Fast path: bgl with GL_UNSIGNED_BYTE, no float conversion, in-place update if not hasattr(self, '_gl_texture_buf') or self._gl_texture_buf[0] == 0: self._gl_texture_buf = bgl.Buffer(bgl.GL_INT, 1) bgl.glGenTextures(1, self._gl_texture_buf) @@ -817,12 +812,43 @@ class KromViewportEngine(bpy.types.RenderEngine): pixel_buffer = bgl.Buffer(bgl.GL_UNSIGNED_BYTE, num_pixels, bytes(pixel_array)) internal_format = getattr(bgl, 'GL_SRGB8_ALPHA8', bgl.GL_RGBA8) - bgl.glTexImage2D(bgl.GL_TEXTURE_2D, 0, internal_format, width, height, 0, - bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, pixel_buffer) + if self._tex_width == width and self._tex_height == height: + bgl.glTexSubImage2D(bgl.GL_TEXTURE_2D, 0, 0, 0, width, height, + bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, pixel_buffer) + else: + bgl.glTexImage2D(bgl.GL_TEXTURE_2D, 0, internal_format, width, height, 0, + bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, pixel_buffer) bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0) self._texture = tex_id + elif HAS_GPU_TEXTURE: + # Blender 4.x+ path: use bpy.Image + gpu.texture.from_image + # from_image returns a cached GPUTexture (no 20ms constructor) + # pixels updated via foreach_set (fast C-level copy) + update() + if self._viewport_image is None or self._viewport_image.size[0] != width or self._viewport_image.size[1] != height: + if self._viewport_image is not None: + bpy.data.images.remove(self._viewport_image) + self._viewport_image = bpy.data.images.new( + "_lnx_viewport", width, height, float_buffer=False) + self._float_buffer = None + if HAS_NUMPY: + pixels[3::4] = 255 + if self._float_buffer is None or len(self._float_buffer) != num_pixels: + self._float_buffer = np.empty(num_pixels, dtype=np.float32) + self._float_buffer[:num_pixels] = pixels[:num_pixels] + self._float_buffer *= (1.0 / 255.0) + self._viewport_image.pixels.foreach_set(self._float_buffer) + else: + pixel_array = array.array('B', pixels[:num_pixels]) + for i in range(3, len(pixel_array), 4): + pixel_array[i] = 255 + float_pixels = [p / 255.0 for p in pixel_array] + self._viewport_image.pixels.foreach_set(float_pixels) + + self._viewport_image.update() + self._texture = gpu.texture.from_image(self._viewport_image) + self._tex_width = width self._tex_height = height @@ -838,11 +864,17 @@ class KromViewportEngine(bpy.types.RenderEngine): """Called by Blender when the engine instance is destroyed.""" self._stop_krom_process() self._restore_overlay() - if not HAS_GPU_TEXTURE and hasattr(self, '_gl_texture_buf') and self._gl_texture_buf[0] != 0: + if HAS_BGL and hasattr(self, '_gl_texture_buf') and self._gl_texture_buf[0] != 0: try: bgl.glDeleteTextures(1, self._gl_texture_buf) except: pass + if self._viewport_image is not None: + try: + bpy.data.images.remove(self._viewport_image) + except: + pass + self._viewport_image = None if self._viewport_id and self._viewport_id in _active_krom_engines: del _active_krom_engines[self._viewport_id] @@ -928,6 +960,7 @@ class KromViewportEngine(bpy.types.RenderEngine): self._last_region_dims = dims self._request_resize(region.width, region.height) + self._write_camera_matrices(context) self._read_krom_camera(context) pixels = self._read_framebuffer() @@ -966,7 +999,8 @@ class KromViewportEngine(bpy.types.RenderEngine): self._batch_dims = dims self._shader.bind() - if HAS_GPU_TEXTURE: + _is_gl_texture = isinstance(self._texture, int) + if not _is_gl_texture: self._shader.uniform_sampler("image", self._texture) else: bgl.glActiveTexture(bgl.GL_TEXTURE0) @@ -975,7 +1009,7 @@ class KromViewportEngine(bpy.types.RenderEngine): self._batch.draw(self._shader) - if not HAS_GPU_TEXTURE: + if _is_gl_texture: bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0) if HAS_GPU_STATE: