""" EU-Utility - UI Rendering Optimizations High-performance UI rendering for 60+ FPS: 1. Dirty region tracking (only repaint changed areas) 2. Frame rate throttling 3. GPU-accelerated rendering where available 4. Layered rendering (background/foreground separation) 5. Text rendering optimization 6. Paint batching """ from typing import Optional, List, Set, Dict, Tuple, Callable from dataclasses import dataclass from enum import Enum import time import threading try: from PyQt6.QtWidgets import QWidget from PyQt6.QtCore import Qt, QTimer, QRect, QSize, QPoint from PyQt6.QtGui import QPainter, QPixmap, QColor, QRegion PYQT_AVAILABLE = True except ImportError: PYQT_AVAILABLE = False class RenderPriority(Enum): """Render priority levels.""" CRITICAL = 0 # Must render immediately HIGH = 1 # Render next frame NORMAL = 2 # Standard priority LOW = 3 # Can be deferred BACKGROUND = 4 # Render when idle @dataclass class DirtyRegion: """Represents a region that needs repainting.""" rect: QRect priority: RenderPriority timestamp: float widget: Optional[QWidget] = None class FrameRateController: """ Controls rendering frame rate for consistent performance. """ def __init__(self, target_fps: float = 60.0): self.target_fps = target_fps self.target_frame_time = 1.0 / target_fps self._last_frame_time = 0 self._frame_times: List[float] = [] self._max_samples = 60 self._lock = threading.Lock() def should_render(self) -> bool: """Check if we should render a new frame.""" now = time.perf_counter() elapsed = now - self._last_frame_time if elapsed >= self.target_frame_time: with self._lock: self._frame_times.append(elapsed) if len(self._frame_times) > self._max_samples: self._frame_times.pop(0) self._last_frame_time = now return True return False def wait_for_frame(self): """Block until next frame should render.""" now = time.perf_counter() elapsed = now - self._last_frame_time sleep_time = max(0, self.target_frame_time - elapsed) if sleep_time > 0: time.sleep(sleep_time) def get_actual_fps(self) -> float: """Get measured FPS.""" with self._lock: if not self._frame_times: return 0 avg_frame_time = sum(self._frame_times) / len(self._frame_times) return 1.0 / avg_frame_time if avg_frame_time > 0 else 0 def get_stats(self) -> Dict: """Get frame rate statistics.""" with self._lock: if not self._frame_times: return {'fps': 0, 'min': 0, 'max': 0, 'avg': 0} return { 'fps': self.get_actual_fps(), 'min': 1.0 / max(self._frame_times) if self._frame_times else 0, 'max': 1.0 / min(self._frame_times) if self._frame_times else 0, 'avg': sum(self._frame_times) / len(self._frame_times) * 1000 # ms } class DirtyRegionTracker: """ Tracks dirty regions for efficient repainting. Only repaints changed areas instead of entire widgets. """ def __init__(self): self._regions: List[DirtyRegion] = [] self._lock = threading.Lock() self._merged_cache: Optional[QRegion] = None def add_dirty_region(self, rect: QRect, priority: RenderPriority = RenderPriority.NORMAL, widget: Optional[QWidget] = None): """Mark a region as dirty.""" with self._lock: self._regions.append(DirtyRegion( rect=rect, priority=priority, timestamp=time.perf_counter(), widget=widget )) self._merged_cache = None # Invalidate cache def get_merged_region(self) -> Optional[QRegion]: """Get merged dirty region (optimized for minimal repaints).""" with self._lock: if self._merged_cache is not None: return self._merged_cache if not self._regions: return None # Sort by priority self._regions.sort(key=lambda r: r.priority.value) # Merge overlapping regions region = QRegion() for dr in self._regions: region = region.united(dr.rect) self._merged_cache = region return region def clear(self): """Clear all dirty regions.""" with self._lock: self._regions.clear() self._merged_cache = None def clear_region(self, rect: QRect): """Clear specific region.""" with self._lock: self._regions = [ r for r in self._regions if not r.rect.intersects(rect) ] self._merged_cache = None def is_dirty(self, rect: QRect) -> bool: """Check if a region is dirty.""" with self._lock: for dr in self._regions: if dr.rect.intersects(rect): return True return False class OptimizedPainter: """ Optimized painting utilities. """ def __init__(self, widget: QWidget): self.widget = widget self._frame_controller = FrameRateController(60.0) self._dirty_tracker = DirtyRegionTracker() self._double_buffer: Optional[QPixmap] = None self._buffer_valid = False def begin_paint(self, painter: QPainter) -> bool: """Begin optimized painting. Returns True if should paint.""" if not self._frame_controller.should_render(): return False # Set rendering hints for performance painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True) painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, False) return True def paint_dirty_regions(self, paint_func: Callable[[QPainter, QRect], None]): """Paint only dirty regions.""" region = self._dirty_tracker.get_merged_region() if region is None: return painter = QPainter(self.widget) try: # Set clip region to dirty areas only painter.setClipRegion(region) # Paint each dirty region for rect in region.rects(): paint_func(painter, rect) finally: painter.end() # Clear painted regions self._dirty_tracker.clear() def invalidate_region(self, rect: QRect, priority: RenderPriority = RenderPriority.NORMAL): """Invalidate a region for repainting.""" self._dirty_tracker.add_dirty_region(rect, priority) self._buffer_valid = False self.widget.update() def get_fps_stats(self) -> Dict: """Get frame rate statistics.""" return self._frame_controller.get_stats() class TextRenderer: """ Optimized text rendering with caching. """ _cache: Dict[str, QPixmap] = {} _cache_size_limit = 100 @classmethod def render_cached(cls, painter: QPainter, text: str, x: int, y: int, font=None, color=None) -> QRect: """ Render text using cache. Returns bounding rect. """ from PyQt6.QtGui import QFontMetrics, QPainter # Build cache key cache_key = f"{text}:{id(font)}:{str(color)}" # Check cache if cache_key in cls._cache: pixmap = cls._cache[cache_key] painter.drawPixmap(x, y, pixmap) return QRect(x, y, pixmap.width(), pixmap.height()) # Measure text metrics = QFontMetrics(font) if font else painter.fontMetrics() rect = metrics.boundingRect(text) # Create pixmap pixmap = QPixmap(rect.width(), rect.height()) pixmap.fill(Qt.GlobalColor.transparent) # Render to pixmap p = QPainter(pixmap) if font: p.setFont(font) if color: p.setPen(color) p.drawText(0, metrics.ascent(), text) p.end() # Cache if not too big if len(cls._cache) < cls._cache_size_limit and rect.width() < 200: cls._cache[cache_key] = pixmap # Draw painter.drawPixmap(x, y, pixmap) return QRect(x, y, rect.width(), rect.height()) @classmethod def clear_cache(cls): """Clear text cache.""" cls._cache.clear() class LayeredRenderer: """ Layered rendering for complex UIs. Separates static background from dynamic foreground. """ def __init__(self, size: QSize): self.size = size self._background: Optional[QPixmap] = None self._foreground: Optional[QPixmap] = None self._background_dirty = True self._foreground_dirty = True def invalidate_background(self): """Mark background as needing redraw.""" self._background_dirty = True def invalidate_foreground(self): """Mark foreground as needing redraw.""" self._foreground_dirty = True def render(self, painter: QPainter, background_func: Callable[[QPainter], None], foreground_func: Callable[[QPainter], None]): """Render layers.""" # Render background if needed if self._background_dirty or self._background is None: self._background = QPixmap(self.size) bg_painter = QPainter(self._background) background_func(bg_painter) bg_painter.end() self._background_dirty = False # Render foreground if self._foreground_dirty or self._foreground is None: self._foreground = QPixmap(self.size) self._foreground.fill(Qt.GlobalColor.transparent) fg_painter = QPainter(self._foreground) foreground_func(fg_painter) fg_painter.end() self._foreground_dirty = False # Composite painter.drawPixmap(0, 0, self._background) painter.drawPixmap(0, 0, self._foreground) def resize(self, size: QSize): """Handle resize.""" self.size = size self._background = None self._foreground = None self._background_dirty = True self._foreground_dirty = True class PaintBatcher: """ Batches paint operations for efficiency. """ def __init__(self, flush_interval_ms: float = 16.0): self._operations: List[Callable] = [] self._flush_interval = flush_interval_ms / 1000.0 self._timer: Optional[QTimer] = None self._lock = threading.Lock() def add(self, operation: Callable): """Add paint operation to batch.""" with self._lock: self._operations.append(operation) if self._timer is None and PYQT_AVAILABLE: from PyQt6.QtCore import QTimer self._timer = QTimer() self._timer.setSingleShot(True) self._timer.timeout.connect(self._flush) self._timer.start(int(self._flush_interval * 1000)) def _flush(self): """Execute all batched operations.""" with self._lock: operations = self._operations self._operations = [] self._timer = None for op in operations: try: op() except Exception as e: print(f"[PaintBatcher] Error: {e}") def flush(self): """Force immediate flush.""" if self._timer: self._timer.stop() self._flush() class GPUAcceleratedWidget: """ Mixin for GPU-accelerated widgets (OpenGL). """ def __init__(self): self._use_opengl = False self._gl_context = None def enable_gpu_acceleration(self): """Enable OpenGL acceleration if available.""" try: from PyQt6.QtOpenGLWidgets import QOpenGLWidget from PyQt6.QtCore import QSurfaceFormat # Request OpenGL 3.3 format = QSurfaceFormat() format.setVersion(3, 3) format.setProfile(QSurfaceFormat.OpenGLContextProfile.CoreProfile) QSurfaceFormat.setDefaultFormat(format) self._use_opengl = True print("[GPU] OpenGL acceleration enabled") except ImportError: print("[GPU] OpenGL not available") self._use_opengl = False def is_gpu_accelerated(self) -> bool: """Check if GPU acceleration is active.""" return self._use_opengl # Global instances _frame_controller = FrameRateController(60.0) _paint_batcher = PaintBatcher() def get_frame_controller() -> FrameRateController: """Get global frame rate controller.""" return _frame_controller def get_paint_batcher() -> PaintBatcher: """Get global paint batcher.""" return _paint_batcher def set_target_fps(fps: float): """Set target frame rate globally.""" _frame_controller.target_fps = fps _frame_controller.target_frame_time = 1.0 / fps # Performance monitoring decorator def measure_paint_time(func): """Decorator to measure paint operation time.""" def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = (time.perf_counter() - start) * 1000 if elapsed > 16.67: # Slower than 60fps print(f"[Paint] Slow paint: {elapsed:.2f}ms in {func.__name__}") return result return wrapper if __name__ == "__main__": # Test frame rate controller controller = FrameRateController(60.0) print("Testing frame rate controller...") for _ in range(10): if controller.should_render(): print(f"Frame rendered (FPS: {controller.get_actual_fps():.1f})") time.sleep(0.01)