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 windowWidth(id: Int): Int;
static function windowHeight(id: Int): Int; static function windowHeight(id: Int): Int;
static function setWindowTitle(id: Int, title: String): Void; static function setWindowTitle(id: Int, title: String): Void;
static function windowSetForeground(id: Int): Void;
static function screenDpi(): Int; static function screenDpi(): Int;
static function systemId(): String; static function systemId(): String;
static function requestShutdown(): Void; 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); g.setImageTexture(voxel_tb2, rts.get(write_sdf).image);
var fa:Float32Array = new Float32Array(Main.voxelgiClipmapCount * 10); var fa:Float32Array = new Float32Array(Main.voxelgiClipmapCount * 10);
for (i in 0...Main.voxelgiClipmapCount) { for (j in 0...Main.voxelgiClipmapCount) {
fa[i * 10] = clipmaps[i].voxelSize; fa[j * 10] = clipmaps[j].voxelSize;
fa[i * 10 + 1] = clipmaps[i].extents.x; fa[j * 10 + 1] = clipmaps[j].extents.x;
fa[i * 10 + 2] = clipmaps[i].extents.y; fa[j * 10 + 2] = clipmaps[j].extents.y;
fa[i * 10 + 3] = clipmaps[i].extents.z; fa[j * 10 + 3] = clipmaps[j].extents.z;
fa[i * 10 + 4] = clipmaps[i].center.x; fa[j * 10 + 4] = clipmaps[j].center.x;
fa[i * 10 + 5] = clipmaps[i].center.y; fa[j * 10 + 5] = clipmaps[j].center.y;
fa[i * 10 + 6] = clipmaps[i].center.z; fa[j * 10 + 6] = clipmaps[j].center.z;
fa[i * 10 + 7] = clipmaps[i].offset_prev.x; fa[j * 10 + 7] = clipmaps[j].offset_prev.x;
fa[i * 10 + 8] = clipmaps[i].offset_prev.y; fa[j * 10 + 8] = clipmaps[j].offset_prev.y;
fa[i * 10 + 9] = clipmaps[i].offset_prev.z; fa[j * 10 + 9] = clipmaps[j].offset_prev.z;
} }
g.setFloats(voxel_ca2, fa); g.setFloats(voxel_ca2, fa);
@ -1407,17 +1407,17 @@ class Inc {
g.setTexture(voxel_tg5, rts.get("voxelsSDF").image); g.setTexture(voxel_tg5, rts.get("voxelsSDF").image);
var fa:Float32Array = new Float32Array(Main.voxelgiClipmapCount * 10); var fa:Float32Array = new Float32Array(Main.voxelgiClipmapCount * 10);
for (i in 0...Main.voxelgiClipmapCount) { for (j in 0...Main.voxelgiClipmapCount) {
fa[i * 10] = clipmaps[i].voxelSize; fa[j * 10] = clipmaps[j].voxelSize;
fa[i * 10 + 1] = clipmaps[i].extents.x; fa[j * 10 + 1] = clipmaps[j].extents.x;
fa[i * 10 + 2] = clipmaps[i].extents.y; fa[j * 10 + 2] = clipmaps[j].extents.y;
fa[i * 10 + 3] = clipmaps[i].extents.z; fa[j * 10 + 3] = clipmaps[j].extents.z;
fa[i * 10 + 4] = clipmaps[i].center.x; fa[j * 10 + 4] = clipmaps[j].center.x;
fa[i * 10 + 5] = clipmaps[i].center.y; fa[j * 10 + 5] = clipmaps[j].center.y;
fa[i * 10 + 6] = clipmaps[i].center.z; fa[j * 10 + 6] = clipmaps[j].center.z;
fa[i * 10 + 7] = clipmaps[i].offset_prev.x; fa[j * 10 + 7] = clipmaps[j].offset_prev.x;
fa[i * 10 + 8] = clipmaps[i].offset_prev.y; fa[j * 10 + 8] = clipmaps[j].offset_prev.y;
fa[i * 10 + 9] = clipmaps[i].offset_prev.z; fa[j * 10 + 9] = clipmaps[j].offset_prev.z;
} }
g.setFloats(voxel_ca5, fa); g.setFloats(voxel_ca5, fa);

View File

@ -415,9 +415,8 @@ class RenderPathDeferred {
t.width = 0; t.width = 0;
t.height = 0; t.height = 0;
t.displayp = Inc.getDisplayp(); t.displayp = Inc.getDisplayp();
t.format = Inc.getHdrFormat(); t.format = "RGBA64";
t.scale = Inc.getSuperSampling(); t.scale = Inc.getSuperSampling();
t.depth_buffer = "main";
path.createRenderTarget(t); path.createRenderTarget(t);
} }
#end #end
@ -869,6 +868,7 @@ class RenderPathDeferred {
#if rp_water #if rp_water
{ {
path.setDepthFrom("tex", "gbuffer1");
path.setTarget("buf"); path.setTarget("buf");
path.bindTarget("tex", "tex"); path.bindTarget("tex", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass"); path.drawShader("shader_datas/copy_pass/copy_pass");
@ -876,6 +876,7 @@ class RenderPathDeferred {
path.bindTarget("_main", "gbufferD"); path.bindTarget("_main", "gbufferD");
path.bindTarget("buf", "tex"); path.bindTarget("buf", "tex");
path.drawShader("shader_datas/water_pass/water_pass"); path.drawShader("shader_datas/water_pass/water_pass");
path.setDepthFrom("tex", "gbuffer0");
} }
#end #end
@ -951,20 +952,34 @@ class RenderPathDeferred {
{ {
if (leenkx.data.Config.raw.rp_ssrefr != false) 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 //save depth
path.setTarget("gbufferD1"); path.setTarget("gbufferD1");
path.bindTarget("_main", "tex"); path.bindTarget("_main", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass"); 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 //save background color
path.setTarget("refr"); path.setTarget("refr");
path.bindTarget("tex", "tex"); path.bindTarget("tex", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass"); path.drawShader("shader_datas/copy_pass/copy_pass");
path.setTarget("gbuffer0", ["tex", "gbuffer_refraction", path.setTarget("gbuffer0", ["gbuffer1", "gbuffer_refraction"]);
#if rp_gbuffer2 "gbuffer1", #end
#if rp_gbuffer_emission "buf", #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_shadowmap #if rp_shadowmap
{ {
@ -976,32 +991,21 @@ class RenderPathDeferred {
} }
#end #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 #if rp_ssrs
path.bindTarget("_main", "gbufferD"); path.bindTarget("_main", "gbufferD");
#end #end
path.drawMeshes("refraction"); path.drawMeshes("refraction");
path.setTarget("buf"); path.setTarget("tex");
path.bindTarget("tex", "tex"); // scene with refractive objects path.bindTarget("refr", "tex");
path.bindTarget("refr", "tex1"); // background without refractive objects
path.bindTarget("_main", "gbufferD");
path.bindTarget("gbufferD1", "gbufferD1"); path.bindTarget("gbufferD1", "gbufferD1");
path.bindTarget("gbuffer0", "gbuffer0"); path.bindTarget("gbuffer0", "gbuffer0");
path.bindTarget("gbuffer1", "tex1");
path.bindTarget("_main", "gbufferD");
path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.bindTarget("gbuffer_refraction", "gbuffer_refraction");
path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass");
path.setTarget("tex");
path.bindTarget("buf", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass");
} }
} }
#end #end
@ -1168,8 +1172,10 @@ class RenderPathDeferred {
#else #else
path.isProbe ? path.setTarget(framebuffer) : path.setTarget("bufa"); path.isProbe ? path.setTarget(framebuffer) : path.setTarget("bufa");
#end #end
#elseif (rp_supersampling == 4)
path.setTarget("bufa");
#elseif rp_fsr1 #elseif rp_fsr1
path.setTarget("buf"); path.setTarget("bufa");
#else #else
path.setTarget(framebuffer); path.setTarget(framebuffer);
#end #end
@ -1182,21 +1188,36 @@ class RenderPathDeferred {
#end #end
path.drawShader("shader_datas/smaa_neighborhood_blend/smaa_neighborhood_blend"); 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 (rp_antialiasing == "TAA")
{ {
if (!path.isProbe) { // No last frame for probe 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("bufa", "tex");
path.bindTarget("taa", "tex2"); path.bindTarget("taa", "tex2");
path.bindTarget("gbuffer2", "sveloc"); path.bindTarget("gbuffer2", "sveloc");
path.drawShader("shader_datas/taa_pass/taa_pass"); 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 #if rp_fsr1
path.setTarget("buf"); path.setTarget("buf");
#else #else
path.setTarget(framebuffer); path.setTarget(framebuffer);
#end #end
path.bindTarget("taa", "tex"); path.bindTarget("bufb", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass"); path.drawShader("shader_datas/copy_pass/copy_pass");
} }
} }
@ -1220,7 +1241,7 @@ class RenderPathDeferred {
var finalTarget = ""; var finalTarget = "";
path.setTarget(finalTarget); path.setTarget(finalTarget);
path.bindTarget(framebuffer, "tex"); path.bindTarget(framebuffer, "tex");
path.drawShader("shader_datas/copy_pass/copy_pass"); path.drawShader("shader_datas/supersample_resolve/supersample_resolve");
} }
#end #end

View File

@ -147,7 +147,7 @@ class RenderPathForward {
t.width = 0; t.width = 0;
t.height = 0; t.height = 0;
t.displayp = Inc.getDisplayp(); t.displayp = Inc.getDisplayp();
t.format = "DEPTH24"; t.format = "R32";
t.scale = Inc.getSuperSampling(); t.scale = Inc.getSuperSampling();
path.createRenderTarget(t); path.createRenderTarget(t);
@ -446,6 +446,18 @@ class RenderPathForward {
path.bindTarget("voxels", "voxels"); path.bindTarget("voxels", "voxels");
#if rp_shadowmap
{
#if lnx_shadowmap_atlas
Inc.bindShadowMapAtlas();
#else
Inc.bindShadowMap();
#end
}
#end
path.drawMeshes("voxel");
Inc.computeVoxelsTemporal(); Inc.computeVoxelsTemporal();
#if (rp_voxels == "Voxel GI") #if (rp_voxels == "Voxel GI")
@ -473,7 +485,7 @@ class RenderPathForward {
#if (rp_ssrefr || lnx_voxelgi_refract) #if (rp_ssrefr || lnx_voxelgi_refract)
{ {
path.setTarget("gbuffer_refraction"); path.setTarget("gbuffer_refraction");
path.clearTarget(0xffff00ff); path.clearTarget(0xffffff00);
} }
#end #end
@ -544,11 +556,21 @@ class RenderPathForward {
{ {
if (leenkx.data.Config.raw.rp_ssrefr != false) 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 //save depth
path.setTarget("gbufferD1"); path.setTarget("gbufferD1");
path.bindTarget("_main", "tex"); path.bindTarget("_main", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass"); 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 //save background color
path.setTarget("refr"); path.setTarget("refr");
path.bindTarget("lbuffer0", "tex"); path.bindTarget("lbuffer0", "tex");
@ -579,18 +601,17 @@ class RenderPathForward {
path.drawMeshes("refraction"); path.drawMeshes("refraction");
path.setTarget("bufa"); path.setTarget("lbuffer0");
path.bindTarget("lbuffer0", "tex"); path.bindTarget("lbuffer0", "tex");
path.bindTarget("refr", "tex1"); path.bindTarget("refr", "tex1");
path.bindTarget("_main", "gbufferD"); path.bindTarget("_main", "gbufferD");
path.bindTarget("gbufferD1", "gbufferD1"); path.bindTarget("gbufferD1", "gbufferD1");
path.bindTarget("lbuffer1", "gbuffer0"); path.bindTarget("lbuffer1", "gbuffer0");
path.bindTarget("lbuffer0", "gbuffer1");
path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.bindTarget("gbuffer_refraction", "gbuffer_refraction");
path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass");
path.setTarget("lbuffer0");
path.bindTarget("bufa", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass");
} }
} }
#end #end
@ -630,53 +651,6 @@ class RenderPathForward {
} }
#end #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 #if rp_bloom
{ {
inline Inc.drawBloom("lbuffer0", bloomDownsampler, bloomUpsampler); inline Inc.drawBloom("lbuffer0", bloomDownsampler, bloomUpsampler);
@ -718,8 +692,10 @@ class RenderPathForward {
#if rp_water #if rp_water
{ {
#if (!kha_opengl)
path.setDepthFrom("lbuffer0", "bufa"); // Unbind depth so we can read it path.setDepthFrom("lbuffer0", "bufa"); // Unbind depth so we can read it
path.depthToRenderTarget.set("main", path.renderTargets.get("buf")); path.depthToRenderTarget.set("main", path.renderTargets.get("buf"));
#end
path.setTarget("bufa"); path.setTarget("bufa");
path.bindTarget("lbuffer0", "tex"); path.bindTarget("lbuffer0", "tex");
@ -730,8 +706,10 @@ class RenderPathForward {
path.bindTarget("bufa", "tex"); path.bindTarget("bufa", "tex");
path.drawShader("shader_datas/water_pass/water_pass"); path.drawShader("shader_datas/water_pass/water_pass");
#if (!kha_opengl)
path.setDepthFrom("lbuffer0", "buf"); // Re-bind depth path.setDepthFrom("lbuffer0", "buf"); // Re-bind depth
path.depthToRenderTarget.set("main", path.renderTargets.get("lbuffer0")); path.depthToRenderTarget.set("main", path.renderTargets.get("lbuffer0"));
#end
} }
#end #end
@ -794,10 +772,20 @@ class RenderPathForward {
path.bindTarget("bufa", "edgesTex"); path.bindTarget("bufa", "edgesTex");
path.drawShader("shader_datas/smaa_blend_weight/smaa_blend_weight"); path.drawShader("shader_datas/smaa_blend_weight/smaa_blend_weight");
#if (rp_supersampling == 4)
path.setTarget("bufa");
#else
path.setTarget(framebuffer); path.setTarget(framebuffer);
#end
path.bindTarget("buf", "colorTex"); path.bindTarget("buf", "colorTex");
path.bindTarget("bufb", "blendTex"); path.bindTarget("bufb", "blendTex");
path.drawShader("shader_datas/smaa_neighborhood_blend/smaa_neighborhood_blend"); 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 #end
@ -805,17 +793,16 @@ class RenderPathForward {
{ {
// FSR1 RCAS sharpening pass applied after AA, expects sRGB [0-1] input // FSR1 RCAS sharpening pass applied after AA, expects sRGB [0-1] input
#if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA"))
#if (rp_supersampling == 4) // SMAA outputs to framebuffer which needs an intermediate buffer
var fsrSource = "buf"; path.setTarget("bufb");
var fsrDest = "buf"; path.bindTarget(framebuffer != "" ? framebuffer : "buf", "tex");
#else path.drawShader("shader_datas/copy_pass/copy_pass");
// SMAA outputs to framebuffer which needs an intermediate buffer var fsrSource = "bufb";
path.setTarget("bufb"); #if (rp_supersampling == 4)
path.bindTarget(framebuffer != "" ? framebuffer : "buf", "tex"); var fsrDest = "buf";
path.drawShader("shader_datas/copy_pass/copy_pass"); #else
var fsrSource = "bufb"; var fsrDest = "";
var fsrDest = ""; #end
#end
#else #else
#if (rp_supersampling == 4) #if (rp_supersampling == 4)
var fsrSource = "buf"; var fsrSource = "buf";

View File

@ -2198,25 +2198,28 @@ class LeenkxExporter:
light_object = light_objects[0] if len(light_objects) > 0 else None light_object = light_objects[0] if len(light_objects) > 0 else None
objtype = light_ref.type objtype = light_ref.type
color = [light_ref.color[0], light_ref.color[1], light_ref.color[2]] color = [light_ref.color[0], light_ref.color[1], light_ref.color[2]]
if light_ref.use_temperature: if bpy.app.version >= (4, 5, 0):
temperature_color = light_ref.temperature_color if light_ref.use_temperature:
color[0] *= temperature_color[0] temperature_color = light_ref.temperature_color
color[1] *= temperature_color[1] color[0] *= temperature_color[0]
color[2] *= temperature_color[2] color[1] *= temperature_color[1]
color[2] *= temperature_color[2]
strength = light_ref.energy * math.pow(2.0, light_ref.exposure) strength = light_ref.energy * math.pow(2.0, light_ref.exposure)
if not light_ref.normalize: if not light_ref.normalize:
area = 0.0 area = 0.0
try: try:
if light_object is not None: if light_object is not None:
area = light_ref.area(matrix_world=light_object.matrix_world) area = light_ref.area(matrix_world=light_object.matrix_world)
else: else:
area = light_ref.area()
except TypeError:
area = light_ref.area() area = light_ref.area()
except TypeError:
area = light_ref.area()
if area > 0.0: if area > 0.0:
strength *= area strength *= area
else:
strength = light_ref.energy
out_light = { out_light = {
'name': object_ref[1]["structName"], 'name': object_ref[1]["structName"],

View File

@ -3,7 +3,6 @@ import os
import queue import queue
import sys import sys
import threading import threading
import time
import types import types
from typing import Dict, Tuple, Callable, Set from typing import Dict, Tuple, Callable, Set
@ -13,6 +12,7 @@ from bpy.app.handlers import persistent
import lnx import lnx
import lnx.api import lnx.api
import lnx.nodes_logic import lnx.nodes_logic
import lnx.render_engine
import lnx.make_state as state import lnx.make_state as state
import lnx.utils import lnx.utils
import lnx.utils_vs import lnx.utils_vs
@ -25,6 +25,7 @@ if lnx.is_reload(__name__):
log = lnx.reload_module(log) log = lnx.reload_module(log)
lnx_nodes = lnx.reload_module(lnx_nodes) lnx_nodes = lnx.reload_module(lnx_nodes)
lnx.nodes_logic = lnx.reload_module(lnx.nodes_logic) lnx.nodes_logic = lnx.reload_module(lnx.nodes_logic)
lnx.render_engine = lnx.reload_module(lnx.render_engine)
make = lnx.reload_module(make) make = lnx.reload_module(make)
state = lnx.reload_module(state) state = lnx.reload_module(state)
props = lnx.reload_module(props) props = lnx.reload_module(props)
@ -33,10 +34,8 @@ if lnx.is_reload(__name__):
else: else:
lnx.enable_reload(__name__) 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] = {} _active_threads: Dict[threading.Thread, Callable] = {}
_last_poll_time = 0.0
_consecutive_empty_polls = 0
_last_render_engine = None _last_render_engine = None
@persistent @persistent
@ -169,6 +168,14 @@ def check_render_engine() -> float:
elif _last_render_engine == 'KROM_VIEWPORT': elif _last_render_engine == 'KROM_VIEWPORT':
try: 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() make.stop_viewport()
except Exception as e: except Exception as e:
log.warn(f'Failed to stop viewport: {e}') log.warn(f'Failed to stop viewport: {e}')
@ -183,113 +190,31 @@ def check_render_engine() -> float:
def poll_threads() -> float: def poll_threads() -> float:
""" """Polls the thread callback queue and processes completed threads."""
Improved thread polling with: # Drain queue into active threads
- 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
try: try:
while True: while True:
thread, callback = make.thread_callback_queue.get(block=False) thread, callback = make.thread_callback_queue.get(block=False)
_active_threads[thread] = callback _active_threads[thread] = callback
new_threads_added += 1
except queue.Empty: except queue.Empty:
pass pass
# Early return if no active threads
if not _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 return 0.25
# Reset empty poll counter when we have active threads # Join and callback all completed threads
_consecutive_empty_polls = 0
# Find completed threads (single pass, no re-queuing)
completed_threads = []
for thread in list(_active_threads.keys()): for thread in list(_active_threads.keys()):
if not thread.is_alive(): if not thread.is_alive():
completed_threads.append(thread) callback = _active_threads.pop(thread)
try:
# Batch process all completed threads thread.join()
if completed_threads: callback()
_process_completed_threads(completed_threads) except Exception as e:
bpy.app.timers.unregister(poll_threads)
# Adaptive timing based on activity level bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True)
active_count = len(_active_threads) raise e
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
def _process_completed_threads(completed_threads: list) -> None: return 0.01
"""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()]
}
loaded_py_libraries: Dict[str, types.ModuleType] = {} 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): def build_viewport(viewport_id, width=1920, height=1080):
"""Build project for viewport""" """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') wrd = bpy.data.worlds.get('Lnx')
if not wrd: if not wrd:
@ -769,6 +769,35 @@ def build_viewport(viewport_id, width=1920, height=1080):
state.is_publish = False state.is_publish = False
state.is_export = 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) log.clear(clear_warnings=True, clear_errors=True)
sdk_path = lnx.utils.get_sdk_path() sdk_path = lnx.utils.get_sdk_path()
@ -817,7 +846,8 @@ def compile_viewport(assets_only=False):
if not wrd.lnx_verbose_output: if not wrd.lnx_verbose_output:
cmd.append("--quiet") 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('--nohaxe')
cmd.append('--noproject') cmd.append('--noproject')
@ -856,7 +886,7 @@ def viewport_build_done():
def play_viewport(viewport_id, width=1920, height=1080): def play_viewport(viewport_id, width=1920, height=1080):
"""Launch Krom in a viewport""" """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: if not viewport_id:
log.error('No viewport_id: play_viewport requires an 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) os.makedirs(sources_path)
# export data same as play() but without setting state.is_play # export data same as play() but without setting state.is_play
state.is_viewport = True
state.target = 'krom' state.target = 'krom'
state.is_publish = False state.is_publish = False
state.is_export = 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) export_data(fp, sdk_path)
compile_viewport(assets_only=(not wrd.lnx_recompile)) compile_viewport(assets_only=(not wrd.lnx_recompile))

View File

@ -11,6 +11,8 @@ import sys
import os import os
import array import array
import atexit import atexit
from mathutils import Quaternion, Matrix, Vector
import lnx.make_state as state
HAS_GPU_STATE = bpy.app.version >= (3, 0, 0) HAS_GPU_STATE = bpy.app.version >= (3, 0, 0)
if not HAS_GPU_STATE: if not HAS_GPU_STATE:
@ -24,22 +26,6 @@ try:
except ImportError: except ImportError:
HAS_NUMPY = False 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': if sys.platform == 'win32':
import mmap import mmap
else: else:
@ -50,8 +36,8 @@ else:
except ImportError: except ImportError:
HAS_POSIX_IPC = False HAS_POSIX_IPC = False
VIEWPORT_BUILD = 0x4B524F4D VIEWPORT_BUILD = 0x52554E54
VIEWPORT_VERSION = 1 VIEWPORT_VERSION = 2
VIEWPORT_SHMEM_NAME_BASE = "KROM_VIEWPORT_FB" VIEWPORT_SHMEM_NAME_BASE = "KROM_VIEWPORT_FB"
def _get_viewport_shmem_name(viewport_id=None): def _get_viewport_shmem_name(viewport_id=None):
@ -118,6 +104,7 @@ MOUSE_BUTTON_MIDDLE = 2
_rendered_mode_pending = False _rendered_mode_pending = False
_rendered_mode_retries = 0 _rendered_mode_retries = 0
_msgbus_owner = object() _msgbus_owner = object()
_make_module = None
@bpy.app.handlers.persistent @bpy.app.handlers.persistent
def _on_depsgraph_update(scene, depsgraph): def _on_depsgraph_update(scene, depsgraph):
@ -184,9 +171,10 @@ class KromViewportEngine(bpy.types.RenderEngine):
bl_idname = 'KROM_VIEWPORT' bl_idname = 'KROM_VIEWPORT'
bl_label = 'Krom Viewport' bl_label = 'Krom Viewport'
bl_use_preview = False bl_use_preview = False
bl_use_gpu_context = True
bl_use_shading_nodes_custom = False 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): def _ensure_initialized(self):
@ -200,6 +188,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._initialized = False self._initialized = False
self._shader = None self._shader = None
self._batch = None self._batch = None
self._batch_dims = None
self._viewport_init_done = True self._viewport_init_done = True
self._rendered_mode_set = False self._rendered_mode_set = False
self._last_shading_mode = None self._last_shading_mode = None
@ -208,6 +197,9 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._krom_proc = None self._krom_proc = None
self._krom_launch_attempted = False self._krom_launch_attempted = False
self._pending_camera_sync = 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) self._engine_id = id(self)
# print(f"New engine instance created: {self._engine_id}") # print(f"New engine instance created: {self._engine_id}")
@ -221,8 +213,34 @@ class KromViewportEngine(bpy.types.RenderEngine):
if current != 'RENDERED': if current != 'RENDERED':
space.shading.type = 'RENDERED' space.shading.type = 'RENDERED'
self._rendered_mode_set = True 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: except Exception as e:
print(f"Failed to set rendered mode: {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): def _handle_mode_switch(self, context):
"""Detect shading mode changes and restart Krom on Solid->Rendered switch if needed.""" """Detect shading mode changes and restart Krom on Solid->Rendered switch if needed."""
@ -326,6 +344,9 @@ class KromViewportEngine(bpy.types.RenderEngine):
except: except:
pass pass
self._krom_proc = None self._krom_proc = None
self._krom_launch_attempted = False
self._initialized = False
self._cleanup_shared_memory()
def _init_shared_memory(self, context=None): def _init_shared_memory(self, context=None):
self._ensure_initialized() self._ensure_initialized()
@ -493,6 +514,27 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._initialized = False 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): def _read_header(self):
"""Read and parse the shared memory header.""" """Read and parse the shared memory header."""
self._ensure_initialized() self._ensure_initialized()
@ -503,10 +545,8 @@ class KromViewportEngine(bpy.types.RenderEngine):
if sys.platform == 'win32': if sys.platform == 'win32':
if not self._shm_ptr: if not self._shm_ptr:
return None return None
# use memmove for safety ctypes.memmove(self._header_buffer, self._shm_ptr, VIEWPORT_HEADER_SIZE)
header_buffer = (ctypes.c_ubyte * VIEWPORT_HEADER_SIZE)() header_data = bytes(self._header_buffer)
ctypes.memmove(header_buffer, self._shm_ptr, VIEWPORT_HEADER_SIZE)
header_data = bytes(header_buffer)
else: else:
self._shm.seek(0) self._shm.seek(0)
header_data = self._shm.read(VIEWPORT_HEADER_SIZE) 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]) ready_flag, fmt = struct.unpack('<II', header_data[24:32])
if build != VIEWPORT_BUILD: if build != VIEWPORT_BUILD:
# shared memory may have been recreated
self._handle_shmem_invalid() self._handle_shmem_invalid()
return None return None
@ -538,21 +577,30 @@ class KromViewportEngine(bpy.types.RenderEngine):
"""Handle invalid/stale shared memory by cleaning up and allowing re-init.""" """Handle invalid/stale shared memory by cleaning up and allowing re-init."""
self._cleanup_shared_memory() self._cleanup_shared_memory()
self._initialized = False 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._texture = None
self._last_frame_id = 0 self._last_frame_id = 0
def _read_framebuffer(self): def _read_framebuffer(self):
"""Read framebuffer from shared memory.""" """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() header = self._read_header()
if not header: if not header:
return None return None
if header['frame_id'] == self._last_frame_id:
return None
if header['ready_flag'] == 0:
return None
width = header['width'] width = header['width']
height = header['height'] height = header['height']
@ -565,15 +613,26 @@ class KromViewportEngine(bpy.types.RenderEngine):
if sys.platform == 'win32': if sys.platform == 'win32':
if not self._shm_ptr: if not self._shm_ptr:
return None return None
pixel_buffer = (ctypes.c_ubyte * pixel_size)() if HAS_NUMPY:
ctypes.memmove(pixel_buffer, self._shm_ptr + VIEWPORT_HEADER_SIZE, pixel_size) # Single copy: shared memory -> numpy array
pixels = bytes(pixel_buffer) 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) ready_bytes = struct.pack('<I', 0)
ctypes.memmove(self._shm_ptr + OFFSET_READY_FLAG, ready_bytes, 4) ctypes.memmove(self._shm_ptr + OFFSET_READY_FLAG, ready_bytes, 4)
else: else:
self._shm.seek(VIEWPORT_HEADER_SIZE) 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.seek(OFFSET_READY_FLAG)
self._shm.write(struct.pack('<I', 0)) self._shm.write(struct.pack('<I', 0))
@ -581,7 +640,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._width = width self._width = width
self._height = height self._height = height
return pixels return pixel_array
except Exception as e: except Exception as e:
self._handle_shmem_invalid() self._handle_shmem_invalid()
print(f"Failed to read framebuffer: {e}") 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.seek(OFFSET_KROM_CAMERA_DIRTY)
self._shm.write(clear_data) self._shm.write(clear_data)
from mathutils import Quaternion, Matrix, Vector
rv3d = context.space_data.region_3d if context.space_data else None rv3d = context.space_data.region_3d if context.space_data else None
if not rv3d: if not rv3d:
return return
@ -734,6 +791,8 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._ensure_initialized() self._ensure_initialized()
if not self._initialized: if not self._initialized:
return return
if self._input_enabled_state == enabled:
return
try: try:
data = struct.pack('<I', 1 if enabled else 0) data = struct.pack('<I', 1 if enabled else 0)
@ -744,6 +803,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
else: else:
self._shm.seek(OFFSET_INPUT_ENABLED) self._shm.seek(OFFSET_INPUT_ENABLED)
self._shm.write(data) self._shm.write(data)
self._input_enabled_state = enabled
except Exception as e: except Exception as e:
pass pass
@ -793,7 +853,8 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._shader = gpu.shader.from_builtin(SHADER_IMAGE) self._shader = gpu.shader.from_builtin(SHADER_IMAGE)
def _update_texture(self, pixels, width, height): 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() self._ensure_initialized()
try: try:
num_pixels = width * height * 4 num_pixels = width * height * 4
@ -801,28 +862,46 @@ class KromViewportEngine(bpy.types.RenderEngine):
if len(pixels) < num_pixels: if len(pixels) < num_pixels:
return return
# clear old texture if dimensions changed if HAS_GPU_TEXTURE:
if hasattr(self, '_tex_width') and hasattr(self, '_tex_height'): if HAS_NUMPY:
if self._tex_width != width or self._tex_height != height: pixels[3::4] = 255
self._texture = None float_array = pixels.astype(np.float32) / 255.0
# TODO: re-investigate LUT for linearization buffer = gpu.types.Buffer('FLOAT', num_pixels, float_array)
if HAS_NUMPY: else:
pixel_array = np.frombuffer(pixels[:num_pixels], dtype=np.uint8).copy() pixel_array = array.array('B', pixels[:num_pixels])
pixel_array[3::4] = 255 for i in range(3, len(pixel_array), 4):
float_array = _srgb_to_linear_lut[pixel_array] pixel_array[i] = 255
float_array[3::4] = 1.0 float_pixels = [p / 255.0 for p in pixel_array]
buffer = gpu.types.Buffer('FLOAT', num_pixels, float_array) buffer = gpu.types.Buffer('FLOAT', num_pixels, float_pixels)
else:
pixel_array = array.array('B', pixels[:num_pixels]) self._texture = gpu.types.GPUTexture((width, height), format='SRGB8_A8', data=buffer)
float_pixels = [] else:
for i, p in enumerate(pixel_array): # bgl path for Blender < 3.0
if i % 4 == 3: # alpha channel if not hasattr(self, '_gl_texture_buf') or self._gl_texture_buf[0] == 0:
float_pixels.append(1.0) self._gl_texture_buf = bgl.Buffer(bgl.GL_INT, 1)
else: bgl.glGenTextures(1, self._gl_texture_buf)
float_pixels.append(_srgb_to_linear_lut_list[p]) tex_id = self._gl_texture_buf[0]
buffer = gpu.types.Buffer('FLOAT', len(float_pixels), float_pixels)
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_width = width
self._tex_height = height self._tex_height = height
@ -833,9 +912,23 @@ class KromViewportEngine(bpy.types.RenderEngine):
def render(self, depsgraph): def render(self, depsgraph):
"""Disabled render method for final renders (F12).""" """Disabled render method for final renders (F12)."""
pass 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): def view_update(self, context, depsgraph):
"""Called when the scene is updated.""" """Called when the scene is updated."""
if state.is_exporting:
return
self._ensure_initialized() self._ensure_initialized()
if not self._init_shared_memory(context): if not self._init_shared_memory(context):
return return
@ -848,6 +941,11 @@ class KromViewportEngine(bpy.types.RenderEngine):
def view_draw(self, context, depsgraph): def view_draw(self, context, depsgraph):
"""Called to draw the viewport.""" """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._ensure_initialized()
self._handle_mode_switch(context) self._handle_mode_switch(context)
@ -872,10 +970,23 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._pending_camera_sync = False self._pending_camera_sync = False
self._sync_camera_once(context) 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) self._set_rendered_mode(context)
# TODO: input forwarding when viewport is active # 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 _active_krom_engine = self # legacy single-engine reference
if self._viewport_id: if self._viewport_id:
@ -886,23 +997,19 @@ class KromViewportEngine(bpy.types.RenderEngine):
if not _input_operator_running: if not _input_operator_running:
bpy.app.timers.register(_start_input_capture, first_interval=0.1) 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 region = context.region
if region and region.width > 0 and region.height > 0: 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) self._read_krom_camera(context)
pixels = self._read_framebuffer() pixels = self._read_framebuffer()
if pixels: if pixels is not None:
self._update_texture(pixels, self._width, self._height) self._update_texture(pixels, self._width, self._height)
if not self._texture: if not self._texture:
@ -917,26 +1024,36 @@ class KromViewportEngine(bpy.types.RenderEngine):
bgl.glDisable(bgl.GL_BLEND) bgl.glDisable(bgl.GL_BLEND)
region = context.region region = context.region
dims = (region.width, region.height)
vertices = ( if self._batch is None or self._batch_dims != dims:
(0, 0), vertices = (
(region.width, 0), (0, 0),
(region.width, region.height), (region.width, 0),
(0, region.height), (region.width, region.height),
) (0, region.height),
tex_coords = ( )
(0, 1), # Flip Y tex_coords = (
(1, 1), (0, 1),
(1, 0), (1, 1),
(0, 0), (1, 0),
) (0, 0),
indices = ((0, 1, 2), (0, 2, 3)) )
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.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) self._batch.draw(self._shader)
batch.draw(self._shader)
if not HAS_GPU_TEXTURE:
bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)
if HAS_GPU_STATE: if HAS_GPU_STATE:
gpu.state.blend_set('NONE') 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 = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
batch.draw(shader) batch.draw(shader)
if HAS_GPU_STATE: if HAS_GPU_STATE:
gpu.state.blend_set('NONE') gpu.state.blend_set('NONE')
else: else:
bgl.glDisable(bgl.GL_BLEND) bgl.glDisable(bgl.GL_BLEND)
self.tag_redraw()
class KROM_OT_viewport_input(bpy.types.Operator): class KROM_OT_viewport_input(bpy.types.Operator):
"""Modal operator to capture and forward input to Krom viewport""" """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): def modal(self, context, event):
global _input_operator_running global _input_operator_running
# check if we should stop
if event.type == 'ESC': if event.type == 'ESC':
try: try:
if self._engine: if self._engine:
@ -1002,22 +1120,13 @@ class KROM_OT_viewport_input(bpy.types.Operator):
if event.type in {'SCREEN_SET', 'SCREEN_CHANGED'}: if event.type in {'SCREEN_SET', 'SCREEN_CHANGED'}:
return {'PASS_THROUGH'} 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'} return {'PASS_THROUGH'}
if context.space_data.type == 'VIEW_3D': engine, area, region = result
if context.engine != 'KROM_VIEWPORT': self._engine = engine
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
try: try:
if not self._engine or not self._engine._initialized: 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 _input_operator_running = False
return {'CANCELLED'} return {'CANCELLED'}
region = context.region # Check if mouse is over a UI region (toolbar, header, panel)
if not region: mouse_x_abs = event.mouse_x
return {'PASS_THROUGH'} 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': # Compute mouse position relative to the WINDOW region
return {'PASS_THROUGH'} mouse_x = mouse_x_abs - region.x
mouse_y = region.height - (mouse_y_abs - region.y)
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
try: try:
if event.type == 'MOUSEMOVE': if event.type == 'MOUSEMOVE':
@ -1128,49 +1219,63 @@ class KROM_OT_viewport_input(bpy.types.Operator):
} }
return key_map.get(blender_key) return key_map.get(blender_key)
def _get_engine_for_area(self, context, event=None): def _find_viewport_under_mouse(self, event):
"""Get the correct engine for the viewport under the mouse cursor.""" """Find the area, WINDOW region, and engine under the mouse cursor.
# Use mouse coordinates to find which viewport the mouse is over Returns (engine, area, region) or None."""
if event: mouse_x = event.mouse_x
mouse_x = event.mouse_x mouse_y = event.mouse_y
mouse_y = event.mouse_y
for window in bpy.context.window_manager.windows: for window in bpy.context.window_manager.windows:
for area in window.screen.areas: for area in window.screen.areas:
if area.type != 'VIEW_3D': if area.type != 'VIEW_3D':
continue 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): area.y <= mouse_y < area.y + area.height):
continue
for space in area.spaces: # Find the WINDOW region for this area
if space.type == 'VIEW_3D': win_region = None
try: for rgn in area.regions:
viewport_id = hex(space.as_pointer())[-6:] if rgn.type == 'WINDOW':
if viewport_id in _active_krom_engines: win_region = rgn
return _active_krom_engines[viewport_id]
except:
pass
break break
if context.space_data: if not win_region:
try: continue
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 _active_viewport_id and _active_viewport_id in _active_krom_engines: # Check if mouse is actually within the WINDOW region bounds
return _active_krom_engines[_active_viewport_id] 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): def invoke(self, context, event):
self._engine = self._get_engine_for_area(context) result = self._find_viewport_under_mouse(event)
if not self._engine: 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'} return {'CANCELLED'}
self._engine = result[0]
context.window_manager.modal_handler_add(self) context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
@ -1192,8 +1297,12 @@ def _start_input_capture():
for region in area.regions: for region in area.regions:
if region.type == 'WINDOW': if region.type == 'WINDOW':
override = {'window': window, 'area': area, 'region': region} override = {'window': window, 'area': area, 'region': region}
with bpy.context.temp_override(**override): if bpy.app.version >= (3, 0, 0):
bpy.ops.krom.viewport_input('INVOKE_DEFAULT') 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 _input_operator_running = True
return None return None
except Exception as e: except Exception as e:
@ -1208,7 +1317,9 @@ def get_panels():
'VIEWLAYER_PT_layer_passes', '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 = [] panels = []
for panel in bpy.types.Panel.__subclasses__(): for panel in bpy.types.Panel.__subclasses__():
@ -1262,6 +1373,12 @@ class KROM_OT_stop_viewport(bpy.types.Operator):
make.stop_viewport(viewport_id) make.stop_viewport(viewport_id)
global _active_krom_engines global _active_krom_engines
if viewport_id in _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] del _active_krom_engines[viewport_id]
if context.space_data and hasattr(context.space_data, 'shading'): if context.space_data and hasattr(context.space_data, 'shading'):
context.space_data.shading.type = 'SOLID' context.space_data.shading.type = 'SOLID'