This commit is contained in:
2026-06-24 20:23:12 -07:00
parent 6b704ff469
commit 37c8779d12
10 changed files with 512 additions and 399 deletions

View File

@ -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;

Binary file not shown.

Binary file not shown.

View File

@ -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);

View File

@ -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

View File

@ -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";

View File

@ -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"],

View File

@ -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] = {}

View File

@ -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))

View File

@ -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('<Q', ctypes.string_at(self._shm_ptr + OFFSET_FRAME_ID, 8))[0]
ready_flag = struct.unpack('<I', ctypes.string_at(self._shm_ptr + OFFSET_READY_FLAG, 4))[0]
return frame_id, ready_flag
else:
self._shm.seek(OFFSET_FRAME_ID)
frame_id = struct.unpack('<Q', self._shm.read(8))[0]
self._shm.seek(OFFSET_READY_FLAG)
ready_flag = struct.unpack('<I', self._shm.read(4))[0]
return frame_id, ready_flag
except Exception:
return None
def _read_header(self):
"""Read and parse the shared memory header."""
self._ensure_initialized()
@ -503,10 +545,8 @@ class KromViewportEngine(bpy.types.RenderEngine):
if sys.platform == 'win32':
if not self._shm_ptr:
return None
# use memmove for safety
header_buffer = (ctypes.c_ubyte * VIEWPORT_HEADER_SIZE)()
ctypes.memmove(header_buffer, self._shm_ptr, VIEWPORT_HEADER_SIZE)
header_data = bytes(header_buffer)
ctypes.memmove(self._header_buffer, self._shm_ptr, VIEWPORT_HEADER_SIZE)
header_data = bytes(self._header_buffer)
else:
self._shm.seek(0)
header_data = self._shm.read(VIEWPORT_HEADER_SIZE)
@ -516,7 +556,6 @@ class KromViewportEngine(bpy.types.RenderEngine):
ready_flag, fmt = struct.unpack('<II', header_data[24:32])
if build != VIEWPORT_BUILD:
# shared memory may have been recreated
self._handle_shmem_invalid()
return None
@ -538,21 +577,30 @@ 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:
try:
bgl.glDeleteTextures(1, self._gl_texture_buf)
except:
pass
self._gl_texture_buf = bgl.Buffer(bgl.GL_INT, 1)
self._texture = None
self._last_frame_id = 0
def _read_framebuffer(self):
"""Read framebuffer from shared memory."""
# Fast path: peek frame_id and ready_flag without reading full header
peek = self._peek_frame_id()
if peek is None:
return None
frame_id, ready_flag = peek
if frame_id == self._last_frame_id or ready_flag == 0:
return None
# New frame available - read full header for dimensions
header = self._read_header()
if not header:
return None
if header['frame_id'] == self._last_frame_id:
return None
if header['ready_flag'] == 0:
return None
width = header['width']
height = header['height']
@ -565,15 +613,26 @@ class KromViewportEngine(bpy.types.RenderEngine):
if sys.platform == 'win32':
if not self._shm_ptr:
return None
pixel_buffer = (ctypes.c_ubyte * pixel_size)()
ctypes.memmove(pixel_buffer, self._shm_ptr + VIEWPORT_HEADER_SIZE, pixel_size)
pixels = bytes(pixel_buffer)
if HAS_NUMPY:
# Single copy: shared memory -> 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('<I', 0)
ctypes.memmove(self._shm_ptr + OFFSET_READY_FLAG, ready_bytes, 4)
else:
self._shm.seek(VIEWPORT_HEADER_SIZE)
pixels = self._shm.read(pixel_size)
raw = self._shm.read(pixel_size)
if HAS_NUMPY:
pixel_array = np.frombuffer(raw, dtype=np.uint8).copy()
else:
pixel_array = raw
self._shm.seek(OFFSET_READY_FLAG)
self._shm.write(struct.pack('<I', 0))
@ -581,7 +640,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._width = width
self._height = height
return pixels
return pixel_array
except Exception as e:
self._handle_shmem_invalid()
print(f"Failed to read framebuffer: {e}")
@ -658,8 +717,6 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._shm.seek(OFFSET_KROM_CAMERA_DIRTY)
self._shm.write(clear_data)
from mathutils import Quaternion, Matrix, Vector
rv3d = context.space_data.region_3d if context.space_data else None
if not rv3d:
return
@ -734,6 +791,8 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._ensure_initialized()
if not self._initialized:
return
if self._input_enabled_state == enabled:
return
try:
data = struct.pack('<I', 1 if enabled else 0)
@ -744,6 +803,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
else:
self._shm.seek(OFFSET_INPUT_ENABLED)
self._shm.write(data)
self._input_enabled_state = enabled
except Exception as e:
pass
@ -793,7 +853,8 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._shader = gpu.shader.from_builtin(SHADER_IMAGE)
def _update_texture(self, pixels, width, height):
"""Create or update the GPU texture with new pixel data."""
"""Create or update the GPU texture with new pixel data.
Uses SRGB8_A8 format for GPU-side sRGB-to-linear conversion."""
self._ensure_initialized()
try:
num_pixels = width * height * 4
@ -801,28 +862,46 @@ class KromViewportEngine(bpy.types.RenderEngine):
if len(pixels) < num_pixels:
return
# clear old texture if dimensions changed
if hasattr(self, '_tex_width') and hasattr(self, '_tex_height'):
if self._tex_width != width or self._tex_height != height:
self._texture = None
# TODO: re-investigate LUT for linearization
if HAS_NUMPY:
pixel_array = np.frombuffer(pixels[:num_pixels], dtype=np.uint8).copy()
pixel_array[3::4] = 255
float_array = _srgb_to_linear_lut[pixel_array]
float_array[3::4] = 1.0
buffer = gpu.types.Buffer('FLOAT', num_pixels, float_array)
else:
pixel_array = array.array('B', pixels[:num_pixels])
float_pixels = []
for i, p in enumerate(pixel_array):
if i % 4 == 3: # alpha channel
float_pixels.append(1.0)
else:
float_pixels.append(_srgb_to_linear_lut_list[p])
buffer = gpu.types.Buffer('FLOAT', len(float_pixels), float_pixels)
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 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)
tex_id = self._gl_texture_buf[0]
bgl.glBindTexture(bgl.GL_TEXTURE_2D, tex_id)
bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_LINEAR)
bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_LINEAR)
if HAS_NUMPY:
pixels[3::4] = 255
pixel_buffer = bgl.Buffer(bgl.GL_UNSIGNED_BYTE, num_pixels, pixels.tobytes())
else:
pixel_array = array.array('B', pixels[:num_pixels])
for i in range(3, len(pixel_array), 4):
pixel_array[i] = 255
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)
bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)
self._texture = tex_id
self._texture = gpu.types.GPUTexture((width, height), format='RGBA32F', data=buffer)
self._tex_width = width
self._tex_height = height
@ -833,9 +912,23 @@ class KromViewportEngine(bpy.types.RenderEngine):
def render(self, depsgraph):
"""Disabled render method for final renders (F12)."""
pass
def free(self):
"""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:
try:
bgl.glDeleteTextures(1, self._gl_texture_buf)
except:
pass
if self._viewport_id and self._viewport_id in _active_krom_engines:
del _active_krom_engines[self._viewport_id]
def view_update(self, context, depsgraph):
"""Called when the scene is updated."""
if state.is_exporting:
return
self._ensure_initialized()
if not self._init_shared_memory(context):
return
@ -848,6 +941,11 @@ class KromViewportEngine(bpy.types.RenderEngine):
def view_draw(self, context, depsgraph):
"""Called to draw the viewport."""
global _active_krom_engine, _active_krom_engines, _active_viewport_id, _input_operator_running, _make_module
if state.is_exporting:
return
self._ensure_initialized()
self._handle_mode_switch(context)
@ -872,10 +970,23 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._pending_camera_sync = False
self._sync_camera_once(context)
# Check if Krom process has died - allow relaunch
if self._krom_launch_attempted and self._viewport_id:
if _make_module is None:
import lnx.make
_make_module = lnx.make
proc = _make_module._viewport_processes.get(self._viewport_id)
if proc is None or proc.poll() is not None:
print(f"Krom process for viewport {self._viewport_id} is gone, resetting for relaunch")
self._krom_launch_attempted = False
self._initialized = False
self._cleanup_shared_memory()
if self._viewport_id in _active_krom_engines:
del _active_krom_engines[self._viewport_id]
self._set_rendered_mode(context)
# TODO: input forwarding when viewport is active
global _active_krom_engine, _active_krom_engines, _active_viewport_id, _input_operator_running
_active_krom_engine = self # legacy single-engine reference
if self._viewport_id:
@ -886,23 +997,19 @@ class KromViewportEngine(bpy.types.RenderEngine):
if not _input_operator_running:
bpy.app.timers.register(_start_input_capture, first_interval=0.1)
else:
if not hasattr(self, '_input_check_counter'):
self._input_check_counter = 0
self._input_check_counter += 1
if self._input_check_counter > 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'