diff --git a/Kha/Backends/Krom/Krom.hx b/Kha/Backends/Krom/Krom.hx index d122f8f3..f8c5d82f 100644 --- a/Kha/Backends/Krom/Krom.hx +++ b/Kha/Backends/Krom/Krom.hx @@ -114,6 +114,7 @@ extern class Krom { static function windowWidth(id: Int): Int; static function windowHeight(id: Int): Int; static function setWindowTitle(id: Int, title: String): Void; + static function windowSetForeground(id: Int): Void; static function screenDpi(): Int; static function systemId(): String; static function requestShutdown(): Void; diff --git a/Krom/Krom.exe b/Krom/Krom.exe index e5ed37d4..98a40c8a 100644 Binary files a/Krom/Krom.exe and b/Krom/Krom.exe differ diff --git a/Krom/Krom_opengl.exe b/Krom/Krom_opengl.exe index ace48615..b33771ca 100644 Binary files a/Krom/Krom_opengl.exe and b/Krom/Krom_opengl.exe differ diff --git a/leenkx/Sources/leenkx/renderpath/Inc.hx b/leenkx/Sources/leenkx/renderpath/Inc.hx index f83d0fb7..c02178fe 100644 --- a/leenkx/Sources/leenkx/renderpath/Inc.hx +++ b/leenkx/Sources/leenkx/renderpath/Inc.hx @@ -1096,17 +1096,17 @@ class Inc { g.setImageTexture(voxel_tb2, rts.get(write_sdf).image); var fa:Float32Array = new Float32Array(Main.voxelgiClipmapCount * 10); - for (i in 0...Main.voxelgiClipmapCount) { - fa[i * 10] = clipmaps[i].voxelSize; - fa[i * 10 + 1] = clipmaps[i].extents.x; - fa[i * 10 + 2] = clipmaps[i].extents.y; - fa[i * 10 + 3] = clipmaps[i].extents.z; - fa[i * 10 + 4] = clipmaps[i].center.x; - fa[i * 10 + 5] = clipmaps[i].center.y; - fa[i * 10 + 6] = clipmaps[i].center.z; - fa[i * 10 + 7] = clipmaps[i].offset_prev.x; - fa[i * 10 + 8] = clipmaps[i].offset_prev.y; - fa[i * 10 + 9] = clipmaps[i].offset_prev.z; + for (j in 0...Main.voxelgiClipmapCount) { + fa[j * 10] = clipmaps[j].voxelSize; + fa[j * 10 + 1] = clipmaps[j].extents.x; + fa[j * 10 + 2] = clipmaps[j].extents.y; + fa[j * 10 + 3] = clipmaps[j].extents.z; + fa[j * 10 + 4] = clipmaps[j].center.x; + fa[j * 10 + 5] = clipmaps[j].center.y; + fa[j * 10 + 6] = clipmaps[j].center.z; + fa[j * 10 + 7] = clipmaps[j].offset_prev.x; + fa[j * 10 + 8] = clipmaps[j].offset_prev.y; + fa[j * 10 + 9] = clipmaps[j].offset_prev.z; } g.setFloats(voxel_ca2, fa); @@ -1407,17 +1407,17 @@ class Inc { g.setTexture(voxel_tg5, rts.get("voxelsSDF").image); var fa:Float32Array = new Float32Array(Main.voxelgiClipmapCount * 10); - for (i in 0...Main.voxelgiClipmapCount) { - fa[i * 10] = clipmaps[i].voxelSize; - fa[i * 10 + 1] = clipmaps[i].extents.x; - fa[i * 10 + 2] = clipmaps[i].extents.y; - fa[i * 10 + 3] = clipmaps[i].extents.z; - fa[i * 10 + 4] = clipmaps[i].center.x; - fa[i * 10 + 5] = clipmaps[i].center.y; - fa[i * 10 + 6] = clipmaps[i].center.z; - fa[i * 10 + 7] = clipmaps[i].offset_prev.x; - fa[i * 10 + 8] = clipmaps[i].offset_prev.y; - fa[i * 10 + 9] = clipmaps[i].offset_prev.z; + for (j in 0...Main.voxelgiClipmapCount) { + fa[j * 10] = clipmaps[j].voxelSize; + fa[j * 10 + 1] = clipmaps[j].extents.x; + fa[j * 10 + 2] = clipmaps[j].extents.y; + fa[j * 10 + 3] = clipmaps[j].extents.z; + fa[j * 10 + 4] = clipmaps[j].center.x; + fa[j * 10 + 5] = clipmaps[j].center.y; + fa[j * 10 + 6] = clipmaps[j].center.z; + fa[j * 10 + 7] = clipmaps[j].offset_prev.x; + fa[j * 10 + 8] = clipmaps[j].offset_prev.y; + fa[j * 10 + 9] = clipmaps[j].offset_prev.z; } g.setFloats(voxel_ca5, fa); diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx b/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx index 61ba7492..3b5140f7 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx @@ -415,9 +415,8 @@ class RenderPathDeferred { t.width = 0; t.height = 0; t.displayp = Inc.getDisplayp(); - t.format = Inc.getHdrFormat(); + t.format = "RGBA64"; t.scale = Inc.getSuperSampling(); - t.depth_buffer = "main"; path.createRenderTarget(t); } #end @@ -869,6 +868,7 @@ class RenderPathDeferred { #if rp_water { + path.setDepthFrom("tex", "gbuffer1"); path.setTarget("buf"); path.bindTarget("tex", "tex"); path.drawShader("shader_datas/copy_pass/copy_pass"); @@ -876,6 +876,7 @@ class RenderPathDeferred { path.bindTarget("_main", "gbufferD"); path.bindTarget("buf", "tex"); path.drawShader("shader_datas/water_pass/water_pass"); + path.setDepthFrom("tex", "gbuffer0"); } #end @@ -951,20 +952,34 @@ class RenderPathDeferred { { if (leenkx.data.Config.raw.rp_ssrefr != false) { + //#if (!kha_opengl) + //path.setDepthFrom("gbuffer0", "gbuffer1"); // Unbind depth so we can read it + //path.depthToRenderTarget.set("main", path.renderTargets.get("tex")); + //#end + //save depth path.setTarget("gbufferD1"); path.bindTarget("_main", "tex"); path.drawShader("shader_datas/copy_pass/copy_pass"); + //#if (!kha_opengl) + //path.setDepthFrom("gbuffer0", "tex"); // Re-bind depth + //path.depthToRenderTarget.set("main", path.renderTargets.get("gbuffer0")); + //#end + //save background color path.setTarget("refr"); path.bindTarget("tex", "tex"); path.drawShader("shader_datas/copy_pass/copy_pass"); - path.setTarget("gbuffer0", ["tex", "gbuffer_refraction", - #if rp_gbuffer2 "gbuffer1", #end - #if rp_gbuffer_emission "buf", #end - ]); + path.setTarget("gbuffer0", ["gbuffer1", "gbuffer_refraction"]); + + #if (rp_voxels != "Off") + path.bindTarget("voxelsOut", "voxels"); + #if (rp_voxels == "Voxel GI" || lnx_voxelgi_shadows) + path.bindTarget("voxelsSDF", "voxelsSDF"); + #end + #end #if rp_shadowmap { @@ -976,32 +991,21 @@ class RenderPathDeferred { } #end - #if (rp_voxels != "Off") - path.bindTarget("voxelsOut", "voxels"); - #if (rp_voxels == "Voxel GI" || lnx_voxelgi_shadows) - path.bindTarget("voxelsSDF", "voxelsSDF"); - #end - #end - #if rp_ssrs path.bindTarget("_main", "gbufferD"); #end path.drawMeshes("refraction"); - path.setTarget("buf"); - path.bindTarget("tex", "tex"); // scene with refractive objects - path.bindTarget("refr", "tex1"); // background without refractive objects - path.bindTarget("_main", "gbufferD"); + path.setTarget("tex"); + path.bindTarget("refr", "tex"); path.bindTarget("gbufferD1", "gbufferD1"); path.bindTarget("gbuffer0", "gbuffer0"); + path.bindTarget("gbuffer1", "tex1"); + path.bindTarget("_main", "gbufferD"); path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); - - path.setTarget("tex"); - path.bindTarget("buf", "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); } } #end @@ -1168,8 +1172,10 @@ class RenderPathDeferred { #else path.isProbe ? path.setTarget(framebuffer) : path.setTarget("bufa"); #end + #elseif (rp_supersampling == 4) + path.setTarget("bufa"); #elseif rp_fsr1 - path.setTarget("buf"); + path.setTarget("bufa"); #else path.setTarget(framebuffer); #end @@ -1182,21 +1188,36 @@ class RenderPathDeferred { #end path.drawShader("shader_datas/smaa_neighborhood_blend/smaa_neighborhood_blend"); + #if ((rp_supersampling == 4) || rp_fsr1) + #if (rp_antialiasing == "SMAA") + path.setTarget("buf"); + path.bindTarget("bufa", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + #end + #end + #if (rp_antialiasing == "TAA") { if (!path.isProbe) { // No last frame for probe - path.setTarget("taa"); + // Write TAA blend to bufb to avoid read-write hazard on taa + path.setTarget("bufb"); path.bindTarget("bufa", "tex"); path.bindTarget("taa", "tex2"); path.bindTarget("gbuffer2", "sveloc"); path.drawShader("shader_datas/taa_pass/taa_pass"); + // Save blended result as history for next frame + path.setTarget("taa"); + path.bindTarget("bufb", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + + // Output to framebuffer #if rp_fsr1 path.setTarget("buf"); #else path.setTarget(framebuffer); #end - path.bindTarget("taa", "tex"); + path.bindTarget("bufb", "tex"); path.drawShader("shader_datas/copy_pass/copy_pass"); } } @@ -1220,7 +1241,7 @@ class RenderPathDeferred { var finalTarget = ""; path.setTarget(finalTarget); path.bindTarget(framebuffer, "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); + path.drawShader("shader_datas/supersample_resolve/supersample_resolve"); } #end diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx index 21875216..d9c6b8e4 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx @@ -147,7 +147,7 @@ class RenderPathForward { t.width = 0; t.height = 0; t.displayp = Inc.getDisplayp(); - t.format = "DEPTH24"; + t.format = "R32"; t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); @@ -446,6 +446,18 @@ class RenderPathForward { path.bindTarget("voxels", "voxels"); + #if rp_shadowmap + { + #if lnx_shadowmap_atlas + Inc.bindShadowMapAtlas(); + #else + Inc.bindShadowMap(); + #end + } + #end + + path.drawMeshes("voxel"); + Inc.computeVoxelsTemporal(); #if (rp_voxels == "Voxel GI") @@ -473,7 +485,7 @@ class RenderPathForward { #if (rp_ssrefr || lnx_voxelgi_refract) { path.setTarget("gbuffer_refraction"); - path.clearTarget(0xffff00ff); + path.clearTarget(0xffffff00); } #end @@ -544,11 +556,21 @@ class RenderPathForward { { if (leenkx.data.Config.raw.rp_ssrefr != false) { + #if (!kha_opengl) + path.setDepthFrom("lbuffer0", "bufa"); // Unbind depth so we can read it + path.depthToRenderTarget.set("main", path.renderTargets.get("buf")); + #end + //save depth path.setTarget("gbufferD1"); path.bindTarget("_main", "tex"); path.drawShader("shader_datas/copy_pass/copy_pass"); + #if (!kha_opengl) + path.setDepthFrom("lbuffer0", "buf"); // Re-bind depth + path.depthToRenderTarget.set("main", path.renderTargets.get("lbuffer0")); + #end + //save background color path.setTarget("refr"); path.bindTarget("lbuffer0", "tex"); @@ -579,18 +601,17 @@ class RenderPathForward { path.drawMeshes("refraction"); - path.setTarget("bufa"); + path.setTarget("lbuffer0"); + path.bindTarget("lbuffer0", "tex"); path.bindTarget("refr", "tex1"); path.bindTarget("_main", "gbufferD"); path.bindTarget("gbufferD1", "gbufferD1"); path.bindTarget("lbuffer1", "gbuffer0"); + path.bindTarget("lbuffer0", "gbuffer1"); path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); - path.setTarget("lbuffer0"); - path.bindTarget("bufa", "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); } } #end @@ -630,53 +651,6 @@ class RenderPathForward { } #end - #if rp_ssrefr - { - if (leenkx.data.Config.raw.rp_ssrefr != false) - { - path.setTarget("gbufferD1"); - path.bindTarget("_main", "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); - - path.setTarget("refr"); - path.bindTarget("lbuffer0", "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); - - path.setTarget("lbuffer0", ["lbuffer1", "gbuffer_refraction"]); - - #if rp_shadowmap - { - #if lnx_shadowmap_atlas - Inc.bindShadowMapAtlas(); - #else - Inc.bindShadowMap(); - #end - } - #end - - #if (rp_voxels != "Off") - path.bindTarget("voxelsOut", "voxels"); - path.bindTarget("voxelsSDF", "voxelsSDF"); - #end - - path.drawMeshes("refraction"); - - path.setTarget("bufa"); - path.bindTarget("lbuffer0", "tex"); - path.bindTarget("refr", "tex1"); - path.bindTarget("_main", "gbufferD"); - path.bindTarget("gbufferD1", "gbufferD1"); - path.bindTarget("lbuffer1", "gbuffer0"); - path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); - - path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); - path.setTarget("lbuffer0"); - path.bindTarget("bufa", "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); - } - } - #end - #if rp_bloom { inline Inc.drawBloom("lbuffer0", bloomDownsampler, bloomUpsampler); @@ -718,8 +692,10 @@ class RenderPathForward { #if rp_water { + #if (!kha_opengl) path.setDepthFrom("lbuffer0", "bufa"); // Unbind depth so we can read it path.depthToRenderTarget.set("main", path.renderTargets.get("buf")); + #end path.setTarget("bufa"); path.bindTarget("lbuffer0", "tex"); @@ -730,8 +706,10 @@ class RenderPathForward { path.bindTarget("bufa", "tex"); path.drawShader("shader_datas/water_pass/water_pass"); + #if (!kha_opengl) path.setDepthFrom("lbuffer0", "buf"); // Re-bind depth path.depthToRenderTarget.set("main", path.renderTargets.get("lbuffer0")); + #end } #end @@ -794,10 +772,20 @@ class RenderPathForward { path.bindTarget("bufa", "edgesTex"); path.drawShader("shader_datas/smaa_blend_weight/smaa_blend_weight"); + #if (rp_supersampling == 4) + path.setTarget("bufa"); + #else path.setTarget(framebuffer); + #end path.bindTarget("buf", "colorTex"); path.bindTarget("bufb", "blendTex"); path.drawShader("shader_datas/smaa_neighborhood_blend/smaa_neighborhood_blend"); + + #if (rp_supersampling == 4) + path.setTarget("buf"); + path.bindTarget("bufa", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + #end } #end @@ -805,17 +793,16 @@ class RenderPathForward { { // FSR1 RCAS sharpening pass applied after AA, expects sRGB [0-1] input #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) - #if (rp_supersampling == 4) - var fsrSource = "buf"; - var fsrDest = "buf"; - #else - // SMAA outputs to framebuffer which needs an intermediate buffer - path.setTarget("bufb"); - path.bindTarget(framebuffer != "" ? framebuffer : "buf", "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); - var fsrSource = "bufb"; - var fsrDest = ""; - #end + // SMAA outputs to framebuffer which needs an intermediate buffer + path.setTarget("bufb"); + path.bindTarget(framebuffer != "" ? framebuffer : "buf", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + var fsrSource = "bufb"; + #if (rp_supersampling == 4) + var fsrDest = "buf"; + #else + var fsrDest = ""; + #end #else #if (rp_supersampling == 4) var fsrSource = "buf"; diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index a77f1677..bb2a3e8a 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -2198,25 +2198,28 @@ class LeenkxExporter: light_object = light_objects[0] if len(light_objects) > 0 else None objtype = light_ref.type color = [light_ref.color[0], light_ref.color[1], light_ref.color[2]] - if light_ref.use_temperature: - temperature_color = light_ref.temperature_color - color[0] *= temperature_color[0] - color[1] *= temperature_color[1] - color[2] *= temperature_color[2] + if bpy.app.version >= (4, 5, 0): + if light_ref.use_temperature: + temperature_color = light_ref.temperature_color + color[0] *= temperature_color[0] + color[1] *= temperature_color[1] + color[2] *= temperature_color[2] - strength = light_ref.energy * math.pow(2.0, light_ref.exposure) - if not light_ref.normalize: - area = 0.0 - try: - if light_object is not None: - area = light_ref.area(matrix_world=light_object.matrix_world) - else: + strength = light_ref.energy * math.pow(2.0, light_ref.exposure) + if not light_ref.normalize: + area = 0.0 + try: + if light_object is not None: + area = light_ref.area(matrix_world=light_object.matrix_world) + else: + area = light_ref.area() + except TypeError: area = light_ref.area() - except TypeError: - area = light_ref.area() - if area > 0.0: - strength *= area + if area > 0.0: + strength *= area + else: + strength = light_ref.energy out_light = { 'name': object_ref[1]["structName"], diff --git a/leenkx/blender/lnx/handlers.py b/leenkx/blender/lnx/handlers.py index 11ddbce3..89c6f2fe 100644 --- a/leenkx/blender/lnx/handlers.py +++ b/leenkx/blender/lnx/handlers.py @@ -3,7 +3,6 @@ import os import queue import sys import threading -import time import types from typing import Dict, Tuple, Callable, Set @@ -13,6 +12,7 @@ from bpy.app.handlers import persistent import lnx import lnx.api import lnx.nodes_logic +import lnx.render_engine import lnx.make_state as state import lnx.utils import lnx.utils_vs @@ -25,6 +25,7 @@ if lnx.is_reload(__name__): log = lnx.reload_module(log) lnx_nodes = lnx.reload_module(lnx_nodes) lnx.nodes_logic = lnx.reload_module(lnx.nodes_logic) + lnx.render_engine = lnx.reload_module(lnx.render_engine) make = lnx.reload_module(make) state = lnx.reload_module(state) props = lnx.reload_module(props) @@ -33,10 +34,8 @@ if lnx.is_reload(__name__): else: lnx.enable_reload(__name__) -# Module-level storage for active threads (eliminates re-queuing overhead) +# Module-level storage for active threads _active_threads: Dict[threading.Thread, Callable] = {} -_last_poll_time = 0.0 -_consecutive_empty_polls = 0 _last_render_engine = None @persistent @@ -169,6 +168,14 @@ def check_render_engine() -> float: elif _last_render_engine == 'KROM_VIEWPORT': try: + for vid, engine in list(lnx.render_engine._active_krom_engines.items()): + try: + engine._restore_overlay() + except: + pass + lnx.render_engine._active_krom_engines.clear() + lnx.render_engine._active_krom_engine = None + lnx.render_engine._active_viewport_id = None make.stop_viewport() except Exception as e: log.warn(f'Failed to stop viewport: {e}') @@ -183,113 +190,31 @@ def check_render_engine() -> float: def poll_threads() -> float: - """ - Improved thread polling with: - - No re-queuing overhead - - Batch processing of completed threads - - Adaptive timing based on activity - - Better memory management - - Simplified logic flow - """ - global _last_poll_time, _consecutive_empty_polls - current_time = time.time() - - # Process all new threads from queue at once (batch processing) - new_threads_added = 0 + """Polls the thread callback queue and processes completed threads.""" + # Drain queue into active threads try: while True: thread, callback = make.thread_callback_queue.get(block=False) _active_threads[thread] = callback - new_threads_added += 1 except queue.Empty: pass - - # Early return if no active threads + if not _active_threads: - _consecutive_empty_polls += 1 - # Adaptive timing: longer intervals when consistently empty - if _consecutive_empty_polls > 10: - return 0.5 # Back off when no activity return 0.25 - - # Reset empty poll counter when we have active threads - _consecutive_empty_polls = 0 - - # Find completed threads (single pass, no re-queuing) - completed_threads = [] + + # Join and callback all completed threads for thread in list(_active_threads.keys()): if not thread.is_alive(): - completed_threads.append(thread) - - # Batch process all completed threads - if completed_threads: - _process_completed_threads(completed_threads) - - # Adaptive timing based on activity level - active_count = len(_active_threads) - if active_count == 0: - return 0.25 - elif active_count <= 3: - return 0.05 # Medium frequency for low activity - else: - return 0.01 # High frequency for high activity + callback = _active_threads.pop(thread) + try: + thread.join() + callback() + except Exception as e: + bpy.app.timers.unregister(poll_threads) + bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True) + raise e -def _process_completed_threads(completed_threads: list) -> None: - """Process a batch of completed threads with robust error handling.""" - for thread in completed_threads: - callback = _active_threads.pop(thread) # Remove from tracking - - try: - thread.join() # Should be instant since thread is dead - callback() - except Exception as e: - # Robust error recovery - _handle_callback_error(e) - continue # Continue processing other threads - - # Explicit cleanup for better memory management - del thread, callback - -def _handle_callback_error(exception: Exception) -> None: - """Centralized error handling with better recovery.""" - try: - # Try to unregister existing timer - bpy.app.timers.unregister(poll_threads) - except ValueError: - pass # Timer wasn't registered, that's fine - - # Re-register timer with slightly longer interval for stability - bpy.app.timers.register(poll_threads, first_interval=0.1, persistent=True) - - # Re-raise the original exception after ensuring timer continuity - raise exception - -def cleanup_polling_system() -> None: - """Optional cleanup function for proper shutdown.""" - global _active_threads, _consecutive_empty_polls - - # Wait for remaining threads to complete (with timeout) - for thread in list(_active_threads.keys()): - if thread.is_alive(): - thread.join(timeout=1.0) # 1 second timeout - - # Clear tracking structures - _active_threads.clear() - _consecutive_empty_polls = 0 - - # Unregister timer - try: - bpy.app.timers.unregister(poll_threads) - except ValueError: - pass - -def get_polling_stats() -> dict: - """Get statistics about the polling system for monitoring.""" - return { - 'active_threads': len(_active_threads), - 'consecutive_empty_polls': _consecutive_empty_polls, - 'thread_ids': [t.ident for t in _active_threads.keys()] - } + return 0.01 loaded_py_libraries: Dict[str, types.ModuleType] = {} diff --git a/leenkx/blender/lnx/make.py b/leenkx/blender/lnx/make.py index 80c54990..0dddba16 100644 --- a/leenkx/blender/lnx/make.py +++ b/leenkx/blender/lnx/make.py @@ -727,7 +727,7 @@ _viewport_proc_build = None # Separate process tracker for viewport builds def build_viewport(viewport_id, width=1920, height=1080): """Build project for viewport""" - global _viewport_build_in_progress, _viewport_pending_launches, profile_time + global _viewport_build_in_progress, _viewport_pending_launches, profile_time, scripts_mtime wrd = bpy.data.worlds.get('Lnx') if not wrd: @@ -769,6 +769,35 @@ def build_viewport(viewport_id, width=1920, height=1080): state.is_publish = False state.is_export = False + # Recompile detection (matching play() behavior) + if not wrd.lnx_cache_build or \ + not os.path.isfile(krom_js_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) + 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: + lnx.utils.fetch_trait_props() + log.clear(clear_warnings=True, clear_errors=True) sdk_path = lnx.utils.get_sdk_path() @@ -817,7 +846,8 @@ def compile_viewport(assets_only=False): if not wrd.lnx_verbose_output: cmd.append("--quiet") - if assets_only: + krom_js_path = lnx.utils.build_dir() + '/debug/krom/krom.js' + if assets_only and os.path.exists(krom_js_path): cmd.append('--nohaxe') cmd.append('--noproject') @@ -856,7 +886,7 @@ def viewport_build_done(): 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 + 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') @@ -895,9 +925,38 @@ def play_viewport(viewport_id, width=1920, height=1080): os.makedirs(sources_path) # export data same as play() but without setting state.is_play + state.is_viewport = True state.target = 'krom' state.is_publish = False state.is_export = False + + if not wrd.lnx_cache_build or \ + not os.path.isfile(krom_js_path) or \ + assets.khafile_defs_last != assets.khafile_defs or \ + state.last_target != state.target: + wrd.lnx_recompile = True + + state.last_target = state.target + + 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] #.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: + lnx.utils.fetch_trait_props() + export_data(fp, sdk_path) compile_viewport(assets_only=(not wrd.lnx_recompile)) diff --git a/leenkx/blender/lnx/render_engine.py b/leenkx/blender/lnx/render_engine.py index 918bf0ff..a77e5814 100644 --- a/leenkx/blender/lnx/render_engine.py +++ b/leenkx/blender/lnx/render_engine.py @@ -11,6 +11,8 @@ import sys import os import array import atexit +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: @@ -24,22 +26,6 @@ try: except ImportError: HAS_NUMPY = False -# TODO: Fix correctly -def _build_srgb_to_linear_lut(): - lut = [] - for i in range(256): - srgb = i / 255.0 - if srgb <= 0.04045: - linear = srgb / 12.92 - else: - linear = ((srgb + 0.055) / 1.055) ** 2.4 - lut.append(linear) - return lut - -_srgb_to_linear_lut_list = _build_srgb_to_linear_lut() -if HAS_NUMPY: - _srgb_to_linear_lut = np.array(_srgb_to_linear_lut_list, dtype=np.float32) - if sys.platform == 'win32': import mmap else: @@ -50,8 +36,8 @@ else: except ImportError: HAS_POSIX_IPC = False -VIEWPORT_BUILD = 0x4B524F4D -VIEWPORT_VERSION = 1 +VIEWPORT_BUILD = 0x52554E54 +VIEWPORT_VERSION = 2 VIEWPORT_SHMEM_NAME_BASE = "KROM_VIEWPORT_FB" def _get_viewport_shmem_name(viewport_id=None): @@ -118,6 +104,7 @@ MOUSE_BUTTON_MIDDLE = 2 _rendered_mode_pending = False _rendered_mode_retries = 0 _msgbus_owner = object() +_make_module = None @bpy.app.handlers.persistent def _on_depsgraph_update(scene, depsgraph): @@ -184,9 +171,10 @@ class KromViewportEngine(bpy.types.RenderEngine): bl_idname = 'KROM_VIEWPORT' bl_label = 'Krom Viewport' bl_use_preview = False - bl_use_gpu_context = True bl_use_shading_nodes_custom = False - bl_use_eevee_viewport = True + if bpy.app.version >= (2, 90, 0): + bl_use_gpu_context = True + bl_use_eevee_viewport = True def _ensure_initialized(self): @@ -200,6 +188,7 @@ class KromViewportEngine(bpy.types.RenderEngine): self._initialized = False self._shader = None self._batch = None + self._batch_dims = None self._viewport_init_done = True self._rendered_mode_set = False self._last_shading_mode = None @@ -208,6 +197,9 @@ class KromViewportEngine(bpy.types.RenderEngine): self._krom_proc = None self._krom_launch_attempted = False self._pending_camera_sync = False + self._input_enabled_state = None + self._last_region_dims = None + self._header_buffer = (ctypes.c_ubyte * VIEWPORT_HEADER_SIZE)() self._engine_id = id(self) # print(f"New engine instance created: {self._engine_id}") @@ -221,8 +213,34 @@ class KromViewportEngine(bpy.types.RenderEngine): if current != 'RENDERED': space.shading.type = 'RENDERED' self._rendered_mode_set = True + # Disable viewport overlay so Krom output is unobstructed + if space and hasattr(space, 'overlay'): + if not hasattr(self, '_overlay_saved'): + self._overlay_saved = space.overlay.show_overlays + space.overlay.show_overlays = False except Exception as e: print(f"Failed to set rendered mode: {e}") + + def _restore_overlay(self, context=None): + try: + if not hasattr(self, '_overlay_saved'): + return + spaces = [] + if context and context.space_data: + spaces = [context.space_data] + else: + for window in bpy.context.window_manager.windows: + for area in window.screen.areas: + if area.type == 'VIEW_3D': + for sp in area.spaces: + if sp.type == 'VIEW_3D': + spaces.append(sp) + for sp in spaces: + if hasattr(sp, 'overlay'): + sp.overlay.show_overlays = self._overlay_saved + del self._overlay_saved + except: + pass def _handle_mode_switch(self, context): """Detect shading mode changes and restart Krom on Solid->Rendered switch if needed.""" @@ -326,6 +344,9 @@ class KromViewportEngine(bpy.types.RenderEngine): except: pass self._krom_proc = None + self._krom_launch_attempted = False + self._initialized = False + self._cleanup_shared_memory() def _init_shared_memory(self, context=None): self._ensure_initialized() @@ -493,6 +514,27 @@ class KromViewportEngine(bpy.types.RenderEngine): self._initialized = False + def _peek_frame_id(self): + """Quick check if a new frame is available without reading full header.""" + self._ensure_initialized() + if not self._initialized: + return None + try: + if sys.platform == 'win32': + if not self._shm_ptr: + return None + frame_id = struct.unpack(' numpy array + pixel_array = np.ctypeslib.as_array( + (ctypes.c_ubyte * pixel_size).from_address( + self._shm_ptr + VIEWPORT_HEADER_SIZE) + ).copy() + else: + pixel_buffer = (ctypes.c_ubyte * pixel_size)() + ctypes.memmove(pixel_buffer, self._shm_ptr + VIEWPORT_HEADER_SIZE, pixel_size) + pixel_array = bytes(pixel_buffer) ready_bytes = struct.pack(' 60: - self._input_check_counter = 0 - bpy.app.timers.register(_start_input_capture, first_interval=0.5) region = context.region if region and region.width > 0 and region.height > 0: - self._request_resize(region.width, region.height) + dims = (region.width, region.height) + if self._last_region_dims != dims: + self._last_region_dims = dims + self._request_resize(region.width, region.height) self._read_krom_camera(context) pixels = self._read_framebuffer() - if pixels: + if pixels is not None: self._update_texture(pixels, self._width, self._height) if not self._texture: @@ -917,26 +1024,36 @@ class KromViewportEngine(bpy.types.RenderEngine): bgl.glDisable(bgl.GL_BLEND) region = context.region - - vertices = ( - (0, 0), - (region.width, 0), - (region.width, region.height), - (0, region.height), - ) - tex_coords = ( - (0, 1), # Flip Y - (1, 1), - (1, 0), - (0, 0), - ) - indices = ((0, 1, 2), (0, 2, 3)) + dims = (region.width, region.height) + if self._batch is None or self._batch_dims != dims: + vertices = ( + (0, 0), + (region.width, 0), + (region.width, region.height), + (0, region.height), + ) + tex_coords = ( + (0, 1), + (1, 1), + (1, 0), + (0, 0), + ) + indices = ((0, 1, 2), (0, 2, 3)) + self._batch = batch_for_shader(self._shader, 'TRIS', {"pos": vertices, "texCoord": tex_coords}, indices=indices) + self._batch_dims = dims self._shader.bind() - self._shader.uniform_sampler("image", self._texture) + if HAS_GPU_TEXTURE: + self._shader.uniform_sampler("image", self._texture) + else: + bgl.glActiveTexture(bgl.GL_TEXTURE0) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, self._texture) + self._shader.uniform_int("image", [0]) - batch = batch_for_shader(self._shader, 'TRIS', {"pos": vertices, "texCoord": tex_coords}, indices=indices) - batch.draw(self._shader) + self._batch.draw(self._shader) + + if not HAS_GPU_TEXTURE: + bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0) if HAS_GPU_STATE: gpu.state.blend_set('NONE') @@ -969,12 +1086,14 @@ class KromViewportEngine(bpy.types.RenderEngine): batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) batch.draw(shader) - + if HAS_GPU_STATE: gpu.state.blend_set('NONE') else: bgl.glDisable(bgl.GL_BLEND) + self.tag_redraw() + class KROM_OT_viewport_input(bpy.types.Operator): """Modal operator to capture and forward input to Krom viewport""" @@ -988,8 +1107,7 @@ class KROM_OT_viewport_input(bpy.types.Operator): def modal(self, context, event): global _input_operator_running - - # check if we should stop + if event.type == 'ESC': try: if self._engine: @@ -1002,22 +1120,13 @@ class KROM_OT_viewport_input(bpy.types.Operator): if event.type in {'SCREEN_SET', 'SCREEN_CHANGED'}: return {'PASS_THROUGH'} - if not context.space_data or not context.area: + # Re-derive area/region/engine from mouse position each event + result = self._find_viewport_under_mouse(event) + if result is None: return {'PASS_THROUGH'} - if context.space_data.type == 'VIEW_3D': - if context.engine != 'KROM_VIEWPORT': - try: - if self._engine: - self._engine._set_input_enabled(False) - except ReferenceError: - pass - _input_operator_running = False - return {'CANCELLED'} - - current_engine = self._get_engine_for_area(context, event) - if current_engine: - self._engine = current_engine + engine, area, region = result + self._engine = engine try: if not self._engine or not self._engine._initialized: @@ -1026,37 +1135,19 @@ class KROM_OT_viewport_input(bpy.types.Operator): _input_operator_running = False return {'CANCELLED'} - region = context.region - if not region: - return {'PASS_THROUGH'} - + # Check if mouse is over a UI region (toolbar, header, panel) + mouse_x_abs = event.mouse_x + mouse_y_abs = event.mouse_y + for rgn in area.regions: + if rgn.type == 'WINDOW': + continue + if (rgn.x <= mouse_x_abs <= rgn.x + rgn.width and + rgn.y <= mouse_y_abs <= rgn.y + rgn.height): + return {'PASS_THROUGH'} - if region.type != 'WINDOW': - return {'PASS_THROUGH'} - - mouse_in_region = (0 <= event.mouse_region_x <= region.width and - 0 <= event.mouse_region_y <= region.height) - - if not mouse_in_region: - return {'PASS_THROUGH'} - - area = context.area - if area: - mouse_x_abs = event.mouse_x - mouse_y_abs = event.mouse_y - for rgn in area.regions: - # skip the main window region where we actually want input - if rgn.type == 'WINDOW': - continue - # check if mouse is over toolbar, header, UI panel, etc. - if (rgn.x <= mouse_x_abs <= rgn.x + rgn.width and - rgn.y <= mouse_y_abs <= rgn.y + rgn.height): - # mouse is over a UI region - pass through - return {'PASS_THROUGH'} - - mouse_x = event.mouse_region_x - # flip Y for Krom - mouse_y = region.height - event.mouse_region_y + # Compute mouse position relative to the WINDOW region + mouse_x = mouse_x_abs - region.x + mouse_y = region.height - (mouse_y_abs - region.y) try: if event.type == 'MOUSEMOVE': @@ -1128,49 +1219,63 @@ class KROM_OT_viewport_input(bpy.types.Operator): } return key_map.get(blender_key) - def _get_engine_for_area(self, context, event=None): - """Get the correct engine for the viewport under the mouse cursor.""" - # Use mouse coordinates to find which viewport the mouse is over - if event: - mouse_x = event.mouse_x - mouse_y = event.mouse_y + def _find_viewport_under_mouse(self, event): + """Find the area, WINDOW region, and engine under the mouse cursor. + Returns (engine, area, region) or None.""" + mouse_x = event.mouse_x + mouse_y = event.mouse_y - for window in bpy.context.window_manager.windows: - for area in window.screen.areas: - if area.type != 'VIEW_3D': - continue + for window in bpy.context.window_manager.windows: + for area in window.screen.areas: + if area.type != 'VIEW_3D': + continue - if (area.x <= mouse_x < area.x + area.width and + if not (area.x <= mouse_x < area.x + area.width and area.y <= mouse_y < area.y + area.height): + continue - for space in area.spaces: - if space.type == 'VIEW_3D': - try: - viewport_id = hex(space.as_pointer())[-6:] - if viewport_id in _active_krom_engines: - return _active_krom_engines[viewport_id] - except: - pass + # Find the WINDOW region for this area + win_region = None + for rgn in area.regions: + if rgn.type == 'WINDOW': + win_region = rgn break - if context.space_data: - try: - viewport_id = hex(context.space_data.as_pointer())[-6:] - if viewport_id in _active_krom_engines: - return _active_krom_engines[viewport_id] - except: - pass + if not win_region: + continue - if _active_viewport_id and _active_viewport_id in _active_krom_engines: - return _active_krom_engines[_active_viewport_id] + # Check if mouse is actually within the WINDOW region bounds + if not (win_region.x <= mouse_x < win_region.x + win_region.width and + win_region.y <= mouse_y < win_region.y + win_region.height): + continue - return _active_krom_engine + for space in area.spaces: + if space.type == 'VIEW_3D': + try: + viewport_id = hex(space.as_pointer())[-6:] + if viewport_id in _active_krom_engines: + return (_active_krom_engines[viewport_id], area, win_region) + except: + pass + break + + return None def invoke(self, context, event): - self._engine = self._get_engine_for_area(context) - if not self._engine: + result = self._find_viewport_under_mouse(event) + if result is None: + # Fallback: use context's viewport + if context.space_data: + try: + viewport_id = hex(context.space_data.as_pointer())[-6:] + if viewport_id in _active_krom_engines: + self._engine = _active_krom_engines[viewport_id] + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + except: + pass return {'CANCELLED'} - + self._engine = result[0] context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} @@ -1192,8 +1297,12 @@ def _start_input_capture(): for region in area.regions: if region.type == 'WINDOW': override = {'window': window, 'area': area, 'region': region} - with bpy.context.temp_override(**override): - bpy.ops.krom.viewport_input('INVOKE_DEFAULT') + if bpy.app.version >= (3, 0, 0): + with bpy.context.temp_override(**override): + bpy.ops.krom.viewport_input('INVOKE_DEFAULT') + _input_operator_running = True + else: + bpy.ops.krom.viewport_input(override, 'INVOKE_DEFAULT') _input_operator_running = True return None except Exception as e: @@ -1208,7 +1317,9 @@ def get_panels(): 'VIEWLAYER_PT_layer_passes', } - compatible_engines = {'BLENDER_RENDER', 'BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'} + compatible_engines = {'BLENDER_RENDER', 'BLENDER_EEVEE'} + if bpy.app.version >= (4, 2, 0): + compatible_engines.add('BLENDER_EEVEE_NEXT') panels = [] for panel in bpy.types.Panel.__subclasses__(): @@ -1262,6 +1373,12 @@ class KROM_OT_stop_viewport(bpy.types.Operator): make.stop_viewport(viewport_id) global _active_krom_engines if viewport_id in _active_krom_engines: + engine = _active_krom_engines[viewport_id] + try: + engine._stop_krom_process() + engine._restore_overlay(context) + except: + pass del _active_krom_engines[viewport_id] if context.space_data and hasattr(context.space_data, 'shading'): context.space_data.shading.type = 'SOLID'