// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using SkiaSharp; using Microsoft.Maui.Platform.Linux.Window; using System.Runtime.InteropServices; namespace Microsoft.Maui.Platform.Linux.Rendering; /// /// GPU-accelerated rendering engine using OpenGL. /// Falls back to software rendering if GPU initialization fails. /// public class GpuRenderingEngine : IDisposable { private readonly X11Window _window; private GRContext? _grContext; private GRBackendRenderTarget? _renderTarget; private SKSurface? _surface; private SKCanvas? _canvas; private bool _disposed; private bool _gpuAvailable; private int _width; private int _height; // Fallback to software rendering private SKBitmap? _softwareBitmap; private SKCanvas? _softwareCanvas; // Dirty region tracking private readonly List _dirtyRegions = new(); private readonly object _dirtyLock = new(); private bool _fullRedrawNeeded = true; private const int MaxDirtyRegions = 32; /// /// Gets whether GPU acceleration is available and active. /// public bool IsGpuAccelerated => _gpuAvailable && _grContext != null; /// /// Gets the current rendering backend name. /// public string BackendName => IsGpuAccelerated ? "OpenGL" : "Software"; public int Width => _width; public int Height => _height; public GpuRenderingEngine(X11Window window) { _window = window; _width = window.Width; _height = window.Height; // Try to initialize GPU rendering _gpuAvailable = TryInitializeGpu(); if (!_gpuAvailable) { Console.WriteLine("[GpuRenderingEngine] GPU not available, using software rendering"); InitializeSoftwareRendering(); } _window.Resized += OnWindowResized; _window.Exposed += OnWindowExposed; } private bool TryInitializeGpu() { try { // Check if we can create an OpenGL context var glInterface = GRGlInterface.Create(); if (glInterface == null) { Console.WriteLine("[GpuRenderingEngine] Failed to create GL interface"); return false; } _grContext = GRContext.CreateGl(glInterface); if (_grContext == null) { Console.WriteLine("[GpuRenderingEngine] Failed to create GR context"); glInterface.Dispose(); return false; } CreateGpuSurface(); Console.WriteLine("[GpuRenderingEngine] GPU acceleration enabled"); return true; } catch (Exception ex) { Console.WriteLine($"[GpuRenderingEngine] GPU initialization failed: {ex.Message}"); return false; } } private void CreateGpuSurface() { if (_grContext == null) return; _renderTarget?.Dispose(); _surface?.Dispose(); var width = Math.Max(1, _width); var height = Math.Max(1, _height); // Create framebuffer info (assuming default framebuffer 0) var framebufferInfo = new GRGlFramebufferInfo(0, SKColorType.Rgba8888.ToGlSizedFormat()); _renderTarget = new GRBackendRenderTarget( width, height, 0, // sample count 8, // stencil bits framebufferInfo); _surface = SKSurface.Create( _grContext, _renderTarget, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888); if (_surface == null) { Console.WriteLine("[GpuRenderingEngine] Failed to create GPU surface, falling back to software"); _gpuAvailable = false; InitializeSoftwareRendering(); return; } _canvas = _surface.Canvas; } private void InitializeSoftwareRendering() { var width = Math.Max(1, _width); var height = Math.Max(1, _height); _softwareBitmap?.Dispose(); _softwareCanvas?.Dispose(); var imageInfo = new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul); _softwareBitmap = new SKBitmap(imageInfo); _softwareCanvas = new SKCanvas(_softwareBitmap); _canvas = _softwareCanvas; } private void OnWindowResized(object? sender, (int Width, int Height) size) { _width = size.Width; _height = size.Height; if (_gpuAvailable && _grContext != null) { CreateGpuSurface(); } else { InitializeSoftwareRendering(); } _fullRedrawNeeded = true; } private void OnWindowExposed(object? sender, EventArgs e) { _fullRedrawNeeded = true; } /// /// Marks a region as needing redraw. /// public void InvalidateRegion(SKRect region) { if (region.IsEmpty || region.Width <= 0 || region.Height <= 0) return; region = SKRect.Intersect(region, new SKRect(0, 0, Width, Height)); if (region.IsEmpty) return; lock (_dirtyLock) { if (_dirtyRegions.Count >= MaxDirtyRegions) { _fullRedrawNeeded = true; _dirtyRegions.Clear(); return; } _dirtyRegions.Add(region); } } /// /// Marks the entire surface as needing redraw. /// public void InvalidateAll() { _fullRedrawNeeded = true; } /// /// Renders the view tree with dirty region optimization. /// public void Render(SkiaView rootView) { if (_canvas == null) return; // Measure and arrange var availableSize = new SKSize(Width, Height); rootView.Measure(availableSize); rootView.Arrange(new SKRect(0, 0, Width, Height)); // Determine regions to redraw List regionsToRedraw; bool isFullRedraw; lock (_dirtyLock) { isFullRedraw = _fullRedrawNeeded || _dirtyRegions.Count == 0; if (isFullRedraw) { regionsToRedraw = new List { new SKRect(0, 0, Width, Height) }; _dirtyRegions.Clear(); _fullRedrawNeeded = false; } else { regionsToRedraw = new List(_dirtyRegions); _dirtyRegions.Clear(); } } // Render each dirty region foreach (var region in regionsToRedraw) { _canvas.Save(); if (!isFullRedraw) { _canvas.ClipRect(region); } // Clear region _canvas.Clear(SKColors.White); // Draw view tree rootView.Draw(_canvas); _canvas.Restore(); } // Draw popup overlays SkiaView.DrawPopupOverlays(_canvas); // Draw modal dialogs if (LinuxDialogService.HasActiveDialog) { LinuxDialogService.DrawDialogs(_canvas, new SKRect(0, 0, Width, Height)); } _canvas.Flush(); // Present to window if (_gpuAvailable && _grContext != null) { _grContext.Submit(); // Swap buffers would happen here via GLX/EGL } else if (_softwareBitmap != null) { var pixels = _softwareBitmap.GetPixels(); if (pixels != IntPtr.Zero) { _window.DrawPixels(pixels, Width, Height, _softwareBitmap.RowBytes); } } } /// /// Gets performance statistics for the GPU context. /// public GpuStats GetStats() { if (_grContext == null) { return new GpuStats { IsGpuAccelerated = false }; } // Get resource cache limits from GRContext _grContext.GetResourceCacheLimits(out var maxResources, out var maxBytes); return new GpuStats { IsGpuAccelerated = true, MaxTextureSize = 4096, // Common default, SkiaSharp doesn't expose this directly ResourceCacheUsedBytes = 0, // Would need to track manually ResourceCacheLimitBytes = maxBytes }; } /// /// Purges unused GPU resources to free memory. /// public void PurgeResources() { _grContext?.PurgeResources(); } public SKCanvas? GetCanvas() => _canvas; protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { _window.Resized -= OnWindowResized; _window.Exposed -= OnWindowExposed; _surface?.Dispose(); _renderTarget?.Dispose(); _grContext?.Dispose(); _softwareBitmap?.Dispose(); _softwareCanvas?.Dispose(); } _disposed = true; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } /// /// GPU performance statistics. /// public class GpuStats { public bool IsGpuAccelerated { get; init; } public int MaxTextureSize { get; init; } public long ResourceCacheUsedBytes { get; init; } public long ResourceCacheLimitBytes { get; init; } public double ResourceCacheUsedMB => ResourceCacheUsedBytes / (1024.0 * 1024.0); public double ResourceCacheLimitMB => ResourceCacheLimitBytes / (1024.0 * 1024.0); }