Source code for zooui.objects.scene.sceneutils.parallel

## ZooUI - Zooming User Interface
## Copyright (C) 2009 David Roberts <d@vidr.cc>
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License
## as published by the Free Software Foundation; either version 3
## of the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see <https://www.gnu.org/licenses/>.

"""SceneParallelRenderer class for parallel rendering operations.

This class encapsulates all parallel rendering functionality that was
previously part of the Scene class, providing better separation of concerns.
"""

import logging
import time
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from zooui.objects.mediaobjects.stringmediaobject import StringMediaObject
    from zooui.objects.scene.scene import Scene

from PySide6 import QtCore
from PySide6.QtGui import QPainter

from zooui.logger import get_logger
from zooui.objects.mediaobjects.mediaobjectsutils.string.parallellayout import ParallelLayoutCalculator

from .prioritybatcher import PriorityBatcher


[docs] class SceneParallelRenderer: """Manages parallel rendering operations for a Scene. This class encapsulates all parallel rendering functionality that was previously part of the Scene class, providing better separation of concerns. Attributes: _scene: Reference to parent Scene _config: Parallel rendering configuration _enabled: Whether parallel rendering is enabled _layout_calculator: Parallel layout calculator instance _priority_batcher: Priority batcher for text objects _last_viewport_center: Last known viewport center _last_viewport_rect: Last known viewport rectangle _stats: Parallel rendering statistics _logger: Logger instance """ def __init__(self, scene: "Scene", config: dict[str, Any] | None = None): """Initialize the SceneParallelRenderer. Args: scene: Reference to parent Scene config: Configuration dictionary (may contain 'parallel_rendering' key) """ self._scene = scene self._config = config.get("parallel_rendering", {}) if config else {} self._enabled: bool = self._config.get("enabled", True) self._layout_calculator: ParallelLayoutCalculator | None = None self._priority_batcher: PriorityBatcher | None = None self._last_viewport_center: tuple[float, float] = (0.0, 0.0) self._last_viewport_rect: QtCore.QRectF | None = None self._stats: dict[str, Any] = { "total_text_objects": 0, "visible_text_objects": 0, "batches_processed": 0, "cache_hit_rate": 0.0, "average_batch_time_ms": 0.0, "total_processing_time_ms": 0.0, } self._logger: logging.Logger = get_logger("SceneParallelRenderer")
[docs] def is_enabled(self) -> bool: """Check if parallel rendering is enabled. Returns: True if parallel rendering is enabled, False otherwise """ return self._enabled
[docs] def initialize(self) -> None: """Initialize parallel rendering components if not already initialized.""" if self._layout_calculator is None: # Use config values or defaults max_workers = self._config.get("max_workers", 4) batch_timeout_ms = self._config.get("batch_timeout_ms", 1000.0) enable_profiling = self._config.get("enable_profiling", False) self._layout_calculator = ParallelLayoutCalculator( max_workers=max_workers, batch_timeout_ms=batch_timeout_ms, enable_profiling=enable_profiling ) if self._priority_batcher is None: # Use config values or defaults batch_size = self._config.get("batch_size", 10) max_batches = self._config.get("max_batches", 10) self._priority_batcher = PriorityBatcher( batch_size=batch_size, max_batches=max_batches, viewport_center=self._last_viewport_center, viewport_rect=self._last_viewport_rect or QtCore.QRectF(0, 0, 100, 100), )
[docs] def update_viewport(self) -> None: """Update viewport information for parallel rendering components.""" if not self._enabled: return # Calculate current viewport center and rectangle viewport_center = (self._scene.centre[0], self._scene.centre[1]) viewport_width, viewport_height = self._scene.viewport_size viewport_rect = QtCore.QRectF( self._scene.centre[0] - viewport_width / 2, self._scene.centre[1] - viewport_height / 2, viewport_width, viewport_height, ) # Check if viewport has changed significantly viewport_changed = ( abs(viewport_center[0] - self._last_viewport_center[0]) > 10.0 or abs(viewport_center[1] - self._last_viewport_center[1]) > 10.0 or self._last_viewport_rect is None ) if viewport_changed: self._last_viewport_center = viewport_center self._last_viewport_rect = viewport_rect # Update priority batcher if initialized if self._priority_batcher: self._priority_batcher.update_viewport(viewport_center, viewport_rect)
[docs] def _get_text_objects(self) -> list["StringMediaObject"]: """Get all StringMediaObjects in the scene. Returns: List of StringMediaObject instances """ # Delegate to Scene's method return self._scene._get_text_objects()
[docs] def precalculate_text_layouts(self) -> None: """Pre-calculate text layouts for parallel rendering. This method should be called before rendering when the scene is moving to prepare layout data for parallel rendering. """ if not self._enabled: return self.initialize() self.update_viewport() # Get text objects text_objects = self._get_text_objects() if not text_objects: return # Update statistics self._stats["total_text_objects"] = len(text_objects) # Add objects to priority batcher if self._priority_batcher: self._priority_batcher.clear_objects() self._priority_batcher.add_objects(text_objects) # Create batches batches = self._priority_batcher.create_batches() self._stats["visible_text_objects"] = self._priority_batcher.get_visible_object_count() # Submit batches for parallel calculation if batches and self._layout_calculator: viewport_rect = self._last_viewport_rect or QtCore.QRectF(0, 0, 100, 100) for batch in batches: # Submit batch for parallel calculation self._layout_calculator.submit_batch( batch, viewport_rect, callback=self._on_layout_calculation_complete )
[docs] def _on_layout_calculation_complete(self, index: int, result: Any) -> None: """Callback for when layout calculation completes. Args: index: Object index result: CalculationResult """ # This callback can be used to update statistics or handle errors # For now, we just update statistics if hasattr(result, "status") and result.status.name == "COMPLETED": self._stats["batches_processed"] += 1
[docs] def render_text(self, painter: QPainter) -> bool: """Render text objects using parallel rendering. Args: painter: QPainter object to render with Returns: True if parallel rendering was used, False otherwise """ if not self._enabled or not self._scene.vzmoving: return False if not self._priority_batcher or not self._layout_calculator: return False # Get batches from priority batcher batches = self._priority_batcher.create_batches() if not batches: return False start_time = time.time() rendered_count = 0 # Render each batch for batch in batches: for prioritized_obj in batch: # Get layout data from cache layout_data = self._priority_batcher.get_layout_cache(prioritized_obj.index) if layout_data and layout_data.is_valid and not layout_data.is_stale(): # Render using layout data obj = prioritized_obj.object # Use string comparison instead of isinstance() for performance and to avoid circular imports # This is consistent with patterns in scene.py and 2.8x faster if type(obj).__name__ == "StringMediaObject": if obj.render_with_layout(painter, layout_data): rendered_count += 1 # Update statistics processing_time = (time.time() - start_time) * 1000 self._stats["total_processing_time_ms"] += processing_time if rendered_count > 0: self._stats["average_batch_time_ms"] = self._stats["total_processing_time_ms"] / max( self._stats["batches_processed"], 1 ) return rendered_count > 0
[docs] def enable(self, enabled: bool = True) -> None: """Enable or disable parallel rendering. Args: enabled: Whether to enable parallel rendering """ self._enabled = enabled if enabled: self.initialize() else: # Clean up resources if self._layout_calculator: self._layout_calculator.shutdown(wait=True) self._layout_calculator = None self._priority_batcher = None
[docs] def shutdown(self) -> None: """Shut down parallel rendering and release resources. Stops the layout calculator and clears the priority batcher. Safe to call multiple times. """ self._enabled = False if self._layout_calculator: self._layout_calculator.shutdown(wait=True) self._layout_calculator = None self._priority_batcher = None
[docs] def get_stats(self) -> dict[str, Any]: """Get parallel rendering statistics. Returns: Dictionary with parallel rendering statistics """ stats = self._stats.copy() # Add current state information stats["parallel_enabled"] = self._enabled stats["scene_moving"] = self._scene.vzmoving if self._priority_batcher: batcher_stats = self._priority_batcher.get_statistics() stats.update( { "batcher_total_objects": batcher_stats["total_objects"], "batcher_visible_objects": batcher_stats["visible_objects"], "batcher_cache_hit_rate": batcher_stats["cache_hit_rate"], "batcher_batch_count": batcher_stats["batch_count"], } ) if self._layout_calculator: calculator_stats = self._layout_calculator.get_statistics() stats.update( { "calculator_queue_size": calculator_stats["queue_size"], "calculator_active_workers": calculator_stats["active_workers"], "calculator_cache_size": calculator_stats["cache_size"], "calculator_cache_hit_rate": calculator_stats["cache_hit_rate"], } ) return stats
[docs] def invalidate_cache(self) -> None: """Invalidate parallel rendering cache.""" if self._priority_batcher: self._priority_batcher.clear_layout_cache() if self._layout_calculator: self._layout_calculator.invalidate_cache() # Also invalidate cache in all text objects text_objects = self._get_text_objects() for obj in text_objects: obj.enable_parallel_rendering(False) # This clears layout cache obj.enable_parallel_rendering(True) # Re-enable