This commit is contained in:
2026-04-27 19:21:50 -07:00
parent 98856b3f54
commit a3d5fa846b
16 changed files with 513 additions and 337 deletions

View File

@ -154,14 +154,14 @@ def _deferred_set_rendered_mode():
_rendered_mode_pending = False
_rendered_mode_retries = 0
return None
space.shading.type = 'RENDERED'
area.tag_redraw()
_rendered_mode_pending = False
_rendered_mode_retries = 0
return None
_rendered_mode_retries += 1
if _rendered_mode_retries < 10:
print(f"Viewport VIEW_3D not found, retrying... ({_rendered_mode_retries})")
@ -232,14 +232,14 @@ class KromViewportEngine(bpy.types.RenderEngine):
return
current_mode = space.shading.type
if self._last_shading_mode == 'SOLID' and current_mode == 'RENDERED':
self._initialized = False
self._rendered_mode_set = False
self._krom_launch_attempted = False
self._cleanup_shared_memory()
self._pending_camera_sync = True
if self._last_shading_mode == 'RENDERED' and current_mode != 'RENDERED':
self._rendered_mode_set = False
# keep krom running in solid mode for switching back ti rendered
@ -260,9 +260,9 @@ class KromViewportEngine(bpy.types.RenderEngine):
view_matrix = rv3d.view_matrix
proj_matrix = rv3d.window_matrix
view_data = struct.pack('<16f', *[view_matrix[i][j] for j in range(4) for i in range(4)])
proj_data = struct.pack('<16f', *[proj_matrix[i][j] for j in range(4) for i in range(4)])
if sys.platform == 'win32':
@ -275,7 +275,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._shm.write(view_data)
self._shm.seek(OFFSET_PROJ_MATRIX)
self._shm.write(proj_data)
except Exception as e:
print(f"Failed camera sync: {e}")
@ -295,11 +295,11 @@ class KromViewportEngine(bpy.types.RenderEngine):
if not self._viewport_id:
import time
self._viewport_id = hex(int(time.time() * 1000) % 0xFFFFFF)[-6:]
region = context.region
width = region.width // 2 if region else 960
height = region.height if region else 540
viewport_id = self._viewport_id
engine_id = self._engine_id
shmem_name = _get_viewport_shmem_name(viewport_id)
@ -331,7 +331,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._ensure_initialized()
if self._initialized:
return True
if self._viewport_id is None and context is not None:
try:
space = context.space_data
@ -339,11 +339,11 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._viewport_id = hex(space.as_pointer())[-6:]
except:
pass
if self._viewport_id is None:
import time
self._viewport_id = hex(int(time.time() * 1000) % 0xFFFFFF)[-6:]
shmem_name = _get_viewport_shmem_name(self._viewport_id)
self._shmem_name = shmem_name
@ -356,7 +356,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
kernel32 = ctypes.windll.kernel32
FILE_MAP_ALL_ACCESS = 0x000F001F
kernel32.OpenFileMappingW.argtypes = [ctypes.wintypes.DWORD, ctypes.wintypes.BOOL, ctypes.wintypes.LPCWSTR]
kernel32.OpenFileMappingW.restype = ctypes.wintypes.HANDLE
@ -371,7 +371,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
kernel32.VirtualQuery.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
kernel32.VirtualQuery.restype = ctypes.c_size_t
handle = kernel32.OpenFileMappingW(
FILE_MAP_ALL_ACCESS,
False,
@ -395,7 +395,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
kernel32.CloseHandle(handle)
print(f"Failed to map shared memory: {error}")
return False
self._shm_handle = handle
self._shm_ptr = ptr
@ -420,11 +420,10 @@ class KromViewportEngine(bpy.types.RenderEngine):
actual_size = mbi.RegionSize
self._shm_size = actual_size
self._shm_buffer = (ctypes.c_ubyte * actual_size).from_address(ptr)
self._initialized = True
# print(f"Connected: {shmem_name}")
return True
except Exception as e:
print(f"Failed to open shared memory: {e}")
@ -451,7 +450,6 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._shm_file = open(shm_path, 'r+b')
self._shm = mmap.mmap(self._shm_file.fileno(), max_size, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)
self._initialized = True
# print(f"Connected: {shm_path}")
return True
except Exception as e:
print(f"Failed to open /dev/shm: {e}")
@ -548,7 +546,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
header = self._read_header()
if not header:
return None
if header['frame_id'] == self._last_frame_id:
return None
@ -570,7 +568,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
pixel_buffer = (ctypes.c_ubyte * pixel_size)()
ctypes.memmove(pixel_buffer, self._shm_ptr + VIEWPORT_HEADER_SIZE, pixel_size)
pixels = bytes(pixel_buffer)
ready_bytes = struct.pack('<I', 0)
ctypes.memmove(self._shm_ptr + OFFSET_READY_FLAG, ready_bytes, 4)
else:
@ -602,9 +600,9 @@ class KromViewportEngine(bpy.types.RenderEngine):
view_matrix = rv3d.view_matrix
proj_matrix = rv3d.window_matrix
view_data = struct.pack('<16f', *[view_matrix[i][j] for j in range(4) for i in range(4)])
proj_data = struct.pack('<16f', *[proj_matrix[i][j] for j in range(4) for i in range(4)])
if sys.platform == 'win32':
@ -640,7 +638,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
dirty = struct.unpack('<I', dirty_data)[0]
if dirty == 0:
return
if sys.platform == 'win32':
pos_data = ctypes.string_at(self._shm_ptr + OFFSET_KROM_CAMERA_POS, 12)
rot_data = ctypes.string_at(self._shm_ptr + OFFSET_KROM_CAMERA_ROT, 16)
@ -652,16 +650,16 @@ class KromViewportEngine(bpy.types.RenderEngine):
pos = struct.unpack('<3f', pos_data)
rot = struct.unpack('<4f', rot_data) # quaternion (x, y, z, w)
clear_data = struct.pack('<I', 0)
if sys.platform == 'win32':
ctypes.memmove(self._shm_ptr + OFFSET_KROM_CAMERA_DIRTY, clear_data, 4)
else:
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
@ -670,12 +668,12 @@ class KromViewportEngine(bpy.types.RenderEngine):
quat = Quaternion((rot[3], rot[0], rot[1], rot[2]))
forward = quat @ Vector((0, 0, -1))
camera_pos = Vector(pos)
view_dist = rv3d.view_distance if rv3d.view_distance > 0 else 10.0
target = camera_pos + forward * view_dist
rv3d.view_location = target
rv3d.view_rotation = quat
@ -687,7 +685,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._ensure_initialized()
if not self._initialized:
return
if hasattr(self, '_last_requested_width') and hasattr(self, '_last_requested_height'):
if self._last_requested_width == width and self._last_requested_height == height:
return
@ -764,23 +762,23 @@ class KromViewportEngine(bpy.types.RenderEngine):
# calculate event offset in ring buffer
event_offset = OFFSET_INPUT_EVENTS + (write_idx % MAX_INPUT_EVENTS) * INPUT_EVENT_SIZE
event_data = struct.pack('<BBhhhI', event_type, button, x, y, delta, modifiers)
ctypes.memmove(self._shm_ptr + event_offset, event_data, len(event_data))
new_write_idx = struct.pack('<I', (write_idx + 1) % MAX_INPUT_EVENTS)
ctypes.memmove(self._shm_ptr + OFFSET_INPUT_WRITE_IDX, new_write_idx, 4)
return True
else:
self._shm.seek(OFFSET_INPUT_WRITE_IDX)
write_idx = struct.unpack('<I', self._shm.read(4))[0]
event_offset = OFFSET_INPUT_EVENTS + (write_idx % MAX_INPUT_EVENTS) * INPUT_EVENT_SIZE
self._shm.seek(event_offset)
event_data = struct.pack('<BBhhhI', event_type, button, x, y, delta, modifiers)
self._shm.write(event_data)
self._shm.seek(OFFSET_INPUT_WRITE_IDX)
self._shm.write(struct.pack('<I', (write_idx + 1) % MAX_INPUT_EVENTS))
return True
@ -799,7 +797,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._ensure_initialized()
try:
num_pixels = width * height * 4
if len(pixels) < num_pixels:
return
@ -823,7 +821,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
else:
float_pixels.append(_srgb_to_linear_lut_list[p])
buffer = gpu.types.Buffer('FLOAT', len(float_pixels), float_pixels)
self._texture = gpu.types.GPUTexture((width, height), format='RGBA32F', data=buffer)
self._tex_width = width
self._tex_height = height
@ -856,11 +854,11 @@ class KromViewportEngine(bpy.types.RenderEngine):
was_initialized = self._initialized
if not self._init_shared_memory(context):
if not self._krom_launch_attempted:
# print(f"Shared memory not found, launching Krom for viewport {self._viewport_id}")
print(f"Shared memory not found, launching Krom for viewport {self._viewport_id}")
self._launch_krom_process(context)
self._draw_placeholder(context)
return
if not was_initialized and self._initialized:
region = context.region
if region and region.width > 0 and region.height > 0:
@ -879,13 +877,13 @@ class KromViewportEngine(bpy.types.RenderEngine):
# 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:
_active_krom_engines[self._viewport_id] = self
_active_viewport_id = self._viewport_id
self._set_input_enabled(True)
if not _input_operator_running:
bpy.app.timers.register(_start_input_capture, first_interval=0.1)
else:
@ -895,13 +893,13 @@ class KromViewportEngine(bpy.types.RenderEngine):
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)
self._read_krom_camera(context)
pixels = self._read_framebuffer()
if pixels:
@ -910,16 +908,16 @@ class KromViewportEngine(bpy.types.RenderEngine):
if not self._texture:
self._draw_placeholder(context)
return
self._create_shader()
if HAS_GPU_STATE:
gpu.state.blend_set('NONE')
else:
bgl.glDisable(bgl.GL_BLEND)
region = context.region
vertices = (
(0, 0),
(region.width, 0),
@ -1000,13 +998,13 @@ class KROM_OT_viewport_input(bpy.types.Operator):
pass
_input_operator_running = False
return {'CANCELLED'}
if event.type in {'SCREEN_SET', 'SCREEN_CHANGED'}:
return {'PASS_THROUGH'}
if not context.space_data or not context.area:
return {'PASS_THROUGH'}
if context.space_data.type == 'VIEW_3D':
if context.engine != 'KROM_VIEWPORT':
try:
@ -1016,18 +1014,18 @@ class KROM_OT_viewport_input(bpy.types.Operator):
pass
_input_operator_running = False
return {'CANCELLED'}
current_engine = self._get_engine_for_area(context, event)
if current_engine:
self._engine = current_engine
try:
if not self._engine or not self._engine._initialized:
return {'PASS_THROUGH'}
except ReferenceError:
_input_operator_running = False
return {'CANCELLED'}
region = context.region
if not region:
return {'PASS_THROUGH'}
@ -1041,7 +1039,7 @@ class KROM_OT_viewport_input(bpy.types.Operator):
if not mouse_in_region:
return {'PASS_THROUGH'}
area = context.area
if area:
mouse_x_abs = event.mouse_x
@ -1059,14 +1057,14 @@ class KROM_OT_viewport_input(bpy.types.Operator):
mouse_x = event.mouse_region_x
# flip Y for Krom
mouse_y = region.height - event.mouse_region_y
try:
if event.type == 'MOUSEMOVE':
self._engine._send_input_event(INPUT_EVENT_MOUSE_MOVE, x=mouse_x, y=mouse_y)
self._last_mouse_x = mouse_x
self._last_mouse_y = mouse_y
return {'RUNNING_MODAL'}
if event.type == 'LEFTMOUSE':
if event.value == 'PRESS':
self._engine._send_input_event(INPUT_EVENT_MOUSE_DOWN, button=MOUSE_BUTTON_LEFT, x=mouse_x, y=mouse_y)
@ -1087,14 +1085,14 @@ class KROM_OT_viewport_input(bpy.types.Operator):
elif event.value == 'RELEASE':
self._engine._send_input_event(INPUT_EVENT_MOUSE_UP, button=MOUSE_BUTTON_MIDDLE, x=mouse_x, y=mouse_y)
return {'RUNNING_MODAL'}
if event.type == 'WHEELUPMOUSE':
self._engine._send_input_event(INPUT_EVENT_MOUSE_WHEEL, delta=1)
return {'RUNNING_MODAL'}
if event.type == 'WHEELDOWNMOUSE':
self._engine._send_input_event(INPUT_EVENT_MOUSE_WHEEL, delta=-1)
return {'RUNNING_MODAL'}
if event.value in {'PRESS', 'RELEASE'}:
key_code = self._blender_to_krom_key(event.type)
if key_code is not None:
@ -1106,7 +1104,7 @@ class KROM_OT_viewport_input(bpy.types.Operator):
except ReferenceError:
_input_operator_running = False
return {'CANCELLED'}
return {'PASS_THROUGH'}
def _blender_to_krom_key(self, blender_key):
@ -1136,15 +1134,15 @@ class KROM_OT_viewport_input(bpy.types.Operator):
if event:
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
if (area.x <= mouse_x < area.x + area.width and
area.y <= mouse_y < area.y + area.height):
for space in area.spaces:
if space.type == 'VIEW_3D':
try:
@ -1154,7 +1152,7 @@ class KROM_OT_viewport_input(bpy.types.Operator):
except:
pass
break
if context.space_data:
try:
viewport_id = hex(context.space_data.as_pointer())[-6:]
@ -1162,10 +1160,10 @@ class KROM_OT_viewport_input(bpy.types.Operator):
return _active_krom_engines[viewport_id]
except:
pass
if _active_viewport_id and _active_viewport_id in _active_krom_engines:
return _active_krom_engines[_active_viewport_id]
return _active_krom_engine
def invoke(self, context, event):
@ -1209,7 +1207,7 @@ def get_panels():
'VIEWLAYER_PT_filter',
'VIEWLAYER_PT_layer_passes',
}
compatible_engines = {'BLENDER_RENDER', 'BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'}
panels = []
@ -1256,7 +1254,7 @@ class KROM_OT_stop_viewport(bpy.types.Operator):
def execute(self, context):
import lnx.make as make
viewport_id = self.viewport_id
if not viewport_id and context.space_data:
viewport_id = hex(context.space_data.as_pointer())[-6:]
@ -1279,12 +1277,12 @@ def draw_viewport_stop_button(self, context):
space = context.space_data
if not space or not hasattr(space, 'shading'):
return
if space.shading.type != 'RENDERED':
return
viewport_id = hex(space.as_pointer())[-6:]
global _active_krom_engines
if viewport_id not in _active_krom_engines:
layout = self.layout
@ -1301,12 +1299,12 @@ def register():
bpy.utils.register_class(KromViewportEngine)
bpy.utils.register_class(KROM_OT_viewport_input)
bpy.utils.register_class(KROM_OT_stop_viewport)
bpy.types.VIEW3D_HT_header.append(draw_viewport_stop_button)
for panel in get_panels():
panel.COMPAT_ENGINES.add('KROM_VIEWPORT')
if _on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update)
atexit.register(_cleanup_krom_on_exit)
@ -1319,21 +1317,21 @@ def unregister():
_active_krom_engines.clear()
_active_viewport_id = None
_input_operator_running = False
if _on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update)
_cleanup_krom_on_exit()
try:
atexit.unregister(_cleanup_krom_on_exit)
except:
pass
for panel in get_panels():
if 'KROM_VIEWPORT' in panel.COMPAT_ENGINES:
panel.COMPAT_ENGINES.remove('KROM_VIEWPORT')
bpy.types.VIEW3D_HT_header.remove(draw_viewport_stop_button)
bpy.utils.unregister_class(KROM_OT_stop_viewport)