This commit is contained in:
2026-02-24 17:35:26 -08:00
parent 1c3c30e6ce
commit d45c632dcd
28 changed files with 1982 additions and 97 deletions

View File

@ -586,6 +586,298 @@ def play_done():
log.clear()
live_patch.stop()
_viewport_processes = {}
def _generate_viewport_id(space_data=None):
"""Generate a unique viewport ID from space_data pointer or random."""
if space_data is not None:
try:
return hex(space_data.as_pointer())[-6:]
except:
pass
import random
return hex(random.randint(0, 0xFFFFFF))[2:].zfill(6)
def _get_viewport_shmem_name(viewport_id):
"""Get shared memory name for a specific viewport."""
return f"KROM_VIEWPORT_FB_{viewport_id}"
def _kill_viewport_process(viewport_id):
"""Kill a specific viewport's Krom process."""
global _viewport_processes
if viewport_id in _viewport_processes:
proc = _viewport_processes[viewport_id]
try:
proc.terminate()
proc.wait(timeout=2)
except:
try:
proc.kill()
except:
pass
del _viewport_processes[viewport_id]
def _kill_all_viewport_processes():
"""Kill all viewport Krom processes."""
global _viewport_processes
for viewport_id in list(_viewport_processes.keys()):
_kill_viewport_process(viewport_id)
if lnx.utils.get_os() == 'win':
try:
result = subprocess.run(
['tasklist', '/FI', 'IMAGENAME eq Krom.exe', '/FO', 'CSV', '/NH'],
capture_output=True, text=True, timeout=5
)
if 'Krom.exe' in result.stdout:
subprocess.run(['taskkill', '/F', '/IM', 'Krom.exe'],
capture_output=True, timeout=5)
import time
time.sleep(0.3)
except:
pass
def run_viewport_runtime(viewport_id, width=1920, height=1080):
"""Launch a viewport which gets its own Krom process with unique shared memory."""
global _viewport_processes
if 'Lnx' not in bpy.data.worlds:
log.warn('No Lnx world found - cannot start viewport server')
return None, None
_kill_viewport_process(viewport_id)
shmem_name = _get_viewport_shmem_name(viewport_id)
wrd = bpy.data.worlds['Lnx']
krom_location, krom_path = lnx.utils.krom_paths()
path = lnx.utils.get_fp_build() + '/debug/krom'
path_resources = path + '-resources'
if not os.path.exists(path + '/krom.js'):
log.warn(f'Krom build not found at {path}/krom.js - build project first')
return None, None
os.chdir(krom_location)
cmd = [krom_path, path, path_resources]
if lnx.utils.get_os() == 'win':
cmd.append('--consolepid')
cmd.append(str(os.getpid()))
if wrd.lnx_audio == 'Disabled':
cmd.append('--nosound')
cmd.append('--viewport-server')
cmd.append('--shmem')
cmd.append(shmem_name)
cmd.append('--viewport-width')
cmd.append(str(width))
cmd.append('--viewport-height')
cmd.append(str(height))
try:
proc = subprocess.Popen(cmd)
_viewport_processes[viewport_id] = proc
log.info(f'Started: {viewport_id} (shmem={shmem_name})')
return proc, shmem_name
except Exception as e:
log.error(f'Failed to start viewport runtime: {e}')
return None, None
def stop_viewport_runtime(viewport_id):
"""Stop a specific viewport's Krom process."""
_kill_viewport_process(viewport_id)
_viewport_build_in_progress = False
_viewport_pending_launches = [] # List of (viewport_id, width, height) tuples
_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
wrd = bpy.data.worlds.get('Lnx')
if not wrd:
log.warn('No Lnx world found - cannot build for viewport')
return
krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js'
if os.path.exists(krom_js_path) and not wrd.lnx_recompile:
log.info(f'Using cached viewport build for {viewport_id}')
run_viewport_runtime(viewport_id, width, height)
return
pending_entry = (viewport_id, width, height)
if pending_entry not in _viewport_pending_launches:
_viewport_pending_launches.append(pending_entry)
log.info(f'Queued viewport {viewport_id} for launch after build')
# If a build is already in progress and there is an actual build process, wait
# _viewport_proc_build checks viewport ,not state.proc_build (which is for play button)
if _viewport_build_in_progress:
if _viewport_proc_build is not None and _viewport_proc_build.poll() is None:
# log.info(f'Build in progress: {viewport_id} - launching when ready')
return
else:
log.info(f'Resetting stale viewport build state')
_viewport_build_in_progress = False
_viewport_build_in_progress = True
profile_time = time.time()
log.info(f'Starting viewport build for {len(_viewport_pending_launches)} viewport(s)')
# Set viewport mode flags but not the is_play flag meant for external launcher
state.is_viewport = True
state.viewport_width = width
state.viewport_height = height
state.target = 'krom'
state.is_play = False # NOT play mode
state.is_publish = False
state.is_export = False
log.clear(clear_warnings=True, clear_errors=True)
sdk_path = lnx.utils.get_sdk_path()
fp = lnx.utils.get_fp()
os.chdir(fp)
sources_path = 'Sources/' + lnx.utils.safestr(wrd.lnx_project_package)
if not os.path.exists(sources_path):
os.makedirs(sources_path)
log.info('Exporting scene data...')
export_data(fp, sdk_path)
log.info('Starting Krom compilation for viewport...')
compile_viewport(assets_only=(not wrd.lnx_recompile))
def compile_viewport(assets_only=False):
"""Compile for viewport mode using separate process tracking."""
global _viewport_proc_build
wrd = bpy.data.worlds['Lnx']
fp = lnx.utils.get_fp()
os.chdir(fp)
node_path = lnx.utils.get_node_path()
khamake_path = lnx.utils.get_khamake_path()
cmd = [node_path, khamake_path, 'krom']
ffmpeg_path = lnx.utils.get_ffmpeg_path()
if ffmpeg_path not in (None, ''):
cmd.append('--ffmpeg')
cmd.append(ffmpeg_path)
cmd.append('-g')
cmd.append(lnx.utils.get_gapi())
cmd.append('--shaderversion')
cmd.append('330')
if lnx.utils.get_khamake_threads() != 1:
cmd.append('--parallelAssetConversion')
cmd.append(str(lnx.utils.get_khamake_threads()))
cmd.append('--to')
cmd.append(lnx.utils.build_dir() + '/debug')
if not wrd.lnx_verbose_output:
cmd.append("--quiet")
if assets_only:
cmd.append('--nohaxe')
cmd.append('--noproject')
log.info(f'Running: {" ".join(cmd)}')
_viewport_proc_build = run_proc(cmd, viewport_build_done)
def viewport_build_done():
"""Called when viewport build completes - launches all pending Krom processes."""
global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build
log.info('Viewport compilation finished')
if _viewport_proc_build is None:
_viewport_build_in_progress = False
return
result = _viewport_proc_build.poll()
_viewport_proc_build = None
_viewport_build_in_progress = False
state.redraw_ui = True
if result == 0:
bpy.data.worlds['Lnx'].lnx_recompile = False
if _viewport_pending_launches:
pending = _viewport_pending_launches.copy()
_viewport_pending_launches.clear()
log.info(f'Launching {len(pending)} Krom instance(s)')
for viewport_id, width, height in pending:
run_viewport_runtime(viewport_id, width, height)
else:
log.info('No pending viewports to launch')
else:
_viewport_pending_launches.clear()
log.error('Viewport build failed, check console')
def play_viewport(viewport_id, width=1920, height=1080):
"""Launch Krom in a viewport"""
global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build
if not viewport_id:
return
if 'Lnx' not in bpy.data.worlds:
return
wrd = bpy.data.worlds['Lnx']
krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js'
if os.path.exists(krom_js_path) and not wrd.lnx_recompile:
run_viewport_runtime(viewport_id, width, height)
return
pending_entry = (viewport_id, width, height)
if pending_entry not in _viewport_pending_launches:
_viewport_pending_launches.append(pending_entry)
if _viewport_build_in_progress:
if _viewport_proc_build is not None and _viewport_proc_build.poll() is None:
log.info(f'Build in progress, viewport {viewport_id} will launch when ready')
return
else:
_viewport_build_in_progress = False # Reset stale state
_viewport_build_in_progress = True
sdk_path = lnx.utils.get_sdk_path()
fp = lnx.utils.get_fp()
os.chdir(fp)
sources_path = 'Sources/' + lnx.utils.safestr(wrd.lnx_project_package)
if not os.path.exists(sources_path):
os.makedirs(sources_path)
# export data same as play() but without setting state.is_play
state.target = 'krom'
state.is_publish = False
state.is_export = False
export_data(fp, sdk_path)
compile_viewport(assets_only=(not wrd.lnx_recompile))
def stop_viewport(viewport_id=None):
"""Stop a specific viewport or all viewports."""
if viewport_id:
_kill_viewport_process(viewport_id)
else:
_kill_all_viewport_processes()
def assets_done():
if state.proc_build == None:
return
@ -761,6 +1053,17 @@ def build_success():
cmd.append(str(pid))
if wrd.lnx_audio == 'Disabled':
cmd.append('--nosound')
if state.is_viewport:
cmd.append('--viewport-server')
cmd.append('--shmem')
if state.viewport_id:
cmd.append(f'KROM_VIEWPORT_FB_{state.viewport_id}')
else:
cmd.append('KROM_VIEWPORT_FB')
cmd.append('--viewport-width')
cmd.append(str(state.viewport_width))
cmd.append('--viewport-height')
cmd.append(str(state.viewport_height))
elif state.target.startswith(('windows-hl', 'linux-hl', 'macos-hl')):
log.info(f"Runtime Hashlink/C target: {state.target}")