Skip to content

Commit

Permalink
- Viewport engine fixes
Browse files Browse the repository at this point in the history
- Fixed: Viewport rendering: objects are not dynamic updating during manipulation edits;
- Fixed: Viewport rendering: Color management changes do not
  • Loading branch information
odil24 authored Jan 1, 2025
1 parent 6ab0d6f commit 40b3ac3
Showing 1 changed file with 92 additions and 69 deletions.
161 changes: 92 additions & 69 deletions engine/viewport.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,151 +6,175 @@
from ..utils import render as utils_render
from ..utils.errorlog import LuxCoreErrorLog
from ..export.config import convert_viewport_engine
import threading
import traceback

# Executed in separate thread
def start_session(engine):
try:
engine.session.Start()
engine.viewport_start_time = time()
except ReferenceError:
# Could not start render session because RenderEngine struct was deleted (caused
# by the user cancelling the viewport render before this function is called)
return
except (pyluxcore.LuxCoreError, RuntimeError) as error:
# Catch LuxCore specific errors or runtime errors
engine.session = None
engine.exporter = None
engine.viewport_fatal_error = f"LuxCoreError: {str(error)}"
LuxCoreErrorLog.add_error(error)
engine.update_stats("Error: ", str(error))
traceback.print_exc()
except Exception as error:
# Generic exception handler
engine.session = None
engine.exporter = None
engine.viewport_fatal_error = f"General Error: {str(error)}"
LuxCoreErrorLog.add_error(error)

engine.update_stats("Error: ", str(error))
LuxCoreErrorLog.add_error(error)

import traceback
traceback.print_exc()

# Note: Due to CPython implementation details, it's not necessary to use a lock here (this modification is atomic)
engine.starting_session = False


def force_session_restart(engine):
"""
For unknown reasons, the old way of handling changes to the renderconfig causes a memory leak.
As a workaround, this method stops and deletes the session to trigger a full re-export of the scene.
"""
if engine.session:
try:
engine.session.Stop()
except Exception as error:
LuxCoreErrorLog.add_error(f"Error stopping session: {error}")
traceback.print_exc()
engine.session = None
engine.exporter = None
if engine.session is not None:
engine.session.Stop()
engine.session = None

def apply_color_management_changes(engine, scene, context):
view_settings = scene.view_settings
exposure = view_settings.exposure
gamma = view_settings.gamma

if engine.session is not None:
config = engine.session.GetRenderConfig()

props = pyluxcore.Properties()
props.Set(pyluxcore.Property("film.imagepipeline.0.gamma", gamma))
props.Set(pyluxcore.Property("film.imagepipeline.0.exposure", exposure))

# Apply the new properties
config.Parse(props)

# Notify LuxCore to update the frame buffer without restarting
if hasattr(engine.session, 'UpdateFilmSettings'):
engine.session.UpdateFilmSettings(props)

def check_color_management_changes(engine, scene):
view_settings = scene.view_settings

# Initialize these attributes if they don't exist yet
if not hasattr(engine, 'last_exposure'):
engine.last_exposure = view_settings.exposure
if not hasattr(engine, 'last_gamma'):
engine.last_gamma = view_settings.gamma
if not hasattr(engine, 'last_view_transform'):
engine.last_view_transform = getattr(view_settings, 'view_transform', None)

# Check if the exposure, gamma, or view transform have changed
exposure_changed = view_settings.exposure != engine.last_exposure
gamma_changed = view_settings.gamma != engine.last_gamma
view_transform_changed = view_settings.view_transform != engine.last_view_transform

# Update the stored values for the next comparison
engine.last_exposure = view_settings.exposure
engine.last_gamma = view_settings.gamma
engine.last_view_transform = view_settings.view_transform

# If any of the values changed, return True
if exposure_changed or gamma_changed or view_transform_changed:
# Handle view transform changes separately
if view_transform_changed:
if view_settings.view_transform in ['AgX', 'Filmic', 'Khronos PBR Neutral']:
# Return False if only the view transform changed, True otherwise
return not (exposure_changed or gamma_changed)
return True

return False

def view_update(engine, context, depsgraph, changes=None):
start = time()

if engine.starting_session or engine.viewport_fatal_error:
# Prevent deadlock
return

LuxCoreErrorLog.clear(force_ui_update=False)

if engine.session is None:
if not engine.viewport_starting_message_shown:
# Let one engine.view_draw() happen so it shows a message in the UI
return

if not engine.is_first_viewport_start:
filmsize = utils.calc_filmsize(depsgraph.scene_eval, context)
filmsize = utils.calc_filmsize(depsgraph.scene, context)
was_resized = engine.last_viewport_size != filmsize
engine.last_viewport_size = filmsize

if was_resized:
engine.time_of_last_viewport_resize = time()
return
elif time() - engine.time_of_last_viewport_resize < engine.VIEWPORT_RESIZE_TIMEOUT:
# Don't re-export the session before the timeout is done
return

try:
print("=" * 50)
print("[Engine/Viewport] New session")
engine.exporter = export.Exporter()
engine.session = engine.exporter.create_session(depsgraph, context, engine=engine)
# Start in separate thread to avoid blocking the UI
engine.starting_session = True
engine.is_first_viewport_start = False
threading.Thread(target=start_session, args=(engine,), daemon=True).start()
except (pyluxcore.LuxCoreError, RuntimeError) as error:
engine.session = None
engine.exporter = None
engine.viewport_fatal_error = f"LuxCoreError: {str(error)}"
LuxCoreErrorLog.add_error(error)
traceback.print_exc()
import _thread
_thread.start_new_thread(start_session, (engine,))
except Exception as error:
engine.session = None
engine.exporter = None
engine.viewport_fatal_error = f"General Error: {str(error)}"
engine.viewport_fatal_error = str(error)

engine.update_stats("Error: ", str(error))
LuxCoreErrorLog.add_error(error)

import traceback
traceback.print_exc()
return

# Check for color management changes
if check_color_management_changes(engine, depsgraph.scene):
apply_color_management_changes(engine, depsgraph.scene, context)
engine.tag_redraw()
return

# Handle changes to the scene, like geometry, camera, etc.
s = time()
# Regular session update logic
changes = engine.exporter.get_changes(depsgraph, context, changes)
print(f"view_update(): checking for changes took {(time() - s) * 1000:.1f} ms")

if changes:
if changes & export.Change.REQUIRES_VIEW_UPDATE:
# Only restart the session if the view transform didn't change by itself
force_session_restart(engine)
return

s = time()
# Update session with changes
engine.session = engine.exporter.update(depsgraph, context, engine.session, changes)
engine.viewport_start_time = time()

if engine.framebuffer:
engine.framebuffer.reset_denoiser()
print(f"view_update(): applying changes took {(time() - s) * 1000:.1f} ms")

print(f"view_update() took {(time() - start) * 1000:.1f} ms")

print("view_update() took %.1f ms" % ((time() - start) * 1000))


def view_draw(engine, context, depsgraph):
scene = depsgraph.scene_eval
scene = depsgraph.scene

if engine.starting_session:
engine.tag_redraw()
return

if engine.viewport_fatal_error:
engine.update_stats("Error:", engine.viewport_fatal_error)
engine.tag_redraw()
return

if engine.session is None:
config = scene.luxcore.config
definitions = {}
luxcore_engine, _ = convert_viewport_engine(context, scene, definitions, config)
message = ""

if luxcore_engine.endswith("OCL"):
# Create dummy renderconfig to check if we have to compile OpenCL kernels
luxcore_scene = pyluxcore.Scene()
definitions = {
"scene.camera.type": "perspective",
}
luxcore_scene.Parse(utils.create_props("", definitions))

devices = scene.luxcore.devices
definitions = {
"renderengine.type": "RTPATHOCL",
Expand All @@ -161,11 +185,11 @@ def view_draw(engine, context, depsgraph):
}
config_props = utils.create_props("", definitions)
renderconfig = pyluxcore.RenderConfig(config_props, luxcore_scene)

if not renderconfig.HasCachedKernels():
gpu_backend = utils.get_addon_preferences(context).gpu_backend
message = f"Compiling {gpu_backend} kernels (just once, usually takes 15-30 minutes)"

engine.update_stats("Starting viewport render", message)
engine.viewport_starting_message_shown = True
engine.tag_update()
Expand All @@ -180,13 +204,14 @@ def view_draw(engine, context, depsgraph):
changes = engine.exporter.get_viewport_changes(depsgraph, context)

if changes & export.Change.REQUIRES_VIEW_UPDATE:
engine.tag_redraw()
force_session_restart(engine)
return
elif changes & export.Change.CAMERA:
engine.session = engine.exporter.update(depsgraph, context, engine.session, export.Change.CAMERA)
elif changes & (export.Change.CAMERA | export.Change.MATERIAL):
# Update session with changes
engine.session = engine.exporter.update(depsgraph, context, engine.session, changes)
engine.viewport_start_time = time()

# Denoising and performance logic
if utils.in_material_shading_mode(context):
if not engine.session.IsInPause():
engine.session.WaitNewFrame()
Expand All @@ -205,6 +230,7 @@ def view_draw(engine, context, depsgraph):
framebuffer.draw(engine, context, scene)
return

# Check if we need to pause the viewport render
rendered_time = time() - engine.viewport_start_time
halt_time = scene.luxcore.viewport.halt_time
status_message = ""
Expand All @@ -230,13 +256,10 @@ def view_draw(engine, context, depsgraph):
framebuffer.start_denoiser(engine.session)
engine.tag_redraw()
except Exception as error:
status_message = f"Could not start denoiser: {error}"
status_message = "Could not start denoiser: %s" % error
else:
engine.session.WaitNewFrame()
try:
engine.session.UpdateStats()
except RuntimeError as error:
print(f"[Engine/Viewport] Error during UpdateStats(): {error}")
engine.session.UpdateStats()
framebuffer.update(engine.session, scene)
framebuffer.reset_denoiser()
engine.tag_redraw()
Expand Down

0 comments on commit 40b3ac3

Please sign in to comment.