// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved. using System; namespace FlaxEngine.GUI { /// /// A common control used to present rendered frame in the UI. /// /// [HideInEditor] public class RenderOutputControl : ContainerControl { /// /// The default back buffer format used by the GUI controls presenting rendered frames. /// public static PixelFormat BackBufferFormat = PixelFormat.R8G8B8A8_UNorm; /// /// The resize check timeout (in seconds). /// public const float ResizeCheckTime = 0.9f; /// /// The task. /// protected SceneRenderTask _task; /// /// The back buffer. /// protected GPUTexture _backBuffer; private GPUTexture _backBufferOld; private int _oldBackbufferLiveTimeLeft; private float _resizeTime; private Int2? _customResolution; /// /// Gets the task. /// public SceneRenderTask Task => _task; /// /// Gets a value indicating whether render to that output only if parent window exists, otherwise false. /// public bool RenderOnlyWithWindow { get; set; } = true; /// /// Gets a value indicating whether keep aspect ratio of the backbuffer image, otherwise false. /// public bool KeepAspectRatio { get; set; } = false; /// /// Gets or sets the color of the tint used to color the backbuffer of the render output. /// public Color TintColor { get; set; } = Color.White; /// /// Gets or sets the brightness of the output. /// public float Brightness { get; set; } = 1.0f; /// /// Gets or sets the rendering resolution scale. Can be sued to upscale image or to downscale the rendering to save the performance. /// public float ResolutionScale { get; set; } = 1.0f; /// /// Gets or sets the custom resolution to use for the rendering. /// public Int2? CustomResolution { get => _customResolution; set { if (_customResolution.HasValue != value.HasValue || (_customResolution.HasValue && _customResolution.Value != value.Value)) { _customResolution = value; SyncBackbufferSize(); } } } /// /// Initializes a new instance of the class. /// /// The task. Cannot be null. /// Invalid task. public RenderOutputControl(SceneRenderTask task) { if (task == null) throw new ArgumentNullException(); _backBuffer = GPUDevice.Instance.CreateTexture(); _resizeTime = ResizeCheckTime; _task = task; _task.Output = _backBuffer; _task.End += OnEnd; Scripting.Update += OnUpdate; } private bool WalkTree(Control c) { while (c != null) { if (c is RootControl) return false; if (c.Visible == false) break; c = c.Parent; } return true; } /// /// Performs a check if rendering a current frame can be skipped (if control size is too small, has missing data, etc.). /// /// True if skip rendering, otherwise false. protected virtual bool CanSkipRendering() { // Disable task rendering if control is very small const float MinRenderSize = 4; if (Width < MinRenderSize || Height < MinRenderSize) return true; // Disable task rendering if control is not used in a window (has using ParentWindow) if (RenderOnlyWithWindow) { return WalkTree(Parent); } return false; } /// /// Called when ask rendering ends. /// /// The task. /// The GPU execution context. protected virtual void OnEnd(RenderTask task, GPUContext context) { // Check if was using old backbuffer if (_backBufferOld) { _oldBackbufferLiveTimeLeft--; if (_oldBackbufferLiveTimeLeft < 0) { Object.Destroy(ref _backBufferOld); } } } private void OnUpdate() { if (_task == null) return; var deltaTime = Time.UnscaledDeltaTime; // Check if need to resize the output _resizeTime += deltaTime; if (_resizeTime >= ResizeCheckTime && Visible && Enabled) { _resizeTime = 0; SyncBackbufferSize(); } // Check if skip rendering var wasEnabled = _task.Enabled; _task.Enabled = !CanSkipRendering(); if (wasEnabled != _task.Enabled) { SyncBackbufferSize(); } } /// public override void Draw() { var bounds = new Rectangle(Vector2.Zero, Size); // Draw background var backgroundColor = BackgroundColor; if (backgroundColor.A > 0.0f) { Render2D.FillRectangle(bounds, backgroundColor); } // Draw backbuffer texture var buffer = _backBufferOld ? _backBufferOld : _backBuffer; var color = TintColor.RGBMultiplied(Brightness); if (KeepAspectRatio) { float ratioX = bounds.Width / buffer.Width; float ratioY = bounds.Height / buffer.Height; float ratio = ratioX < ratioY ? ratioX : ratioY; bounds = new Rectangle((bounds.Width - buffer.Width * ratio) / 2, (bounds.Height - buffer.Height * ratio) / 2, buffer.Width * ratio, buffer.Height * ratio); } Render2D.DrawTexture(buffer, bounds, color); // Push clipping mask if (ClipChildren) { GetDesireClientArea(out var clientArea); Render2D.PushClip(ref clientArea); } DrawChildren(); // Pop clipping mask if (ClipChildren) { Render2D.PopClip(); } } /// /// Synchronizes size of the back buffer with the size of the control. /// public void SyncBackbufferSize() { float scale = ResolutionScale * Platform.DpiScale; int width = Mathf.CeilToInt(Width * scale); int height = Mathf.CeilToInt(Height * scale); if (_customResolution.HasValue) { width = _customResolution.Value.X; height = _customResolution.Value.Y; } if (_backBuffer == null || _backBuffer.Width == width && _backBuffer.Height == height) return; if (width < 1 || height < 1) { _backBuffer.ReleaseGPU(); Object.Destroy(ref _backBufferOld); return; } // Cache old backbuffer to remove flickering effect if (_backBufferOld == null && _backBuffer.IsAllocated) { _backBufferOld = _backBuffer; _backBuffer = GPUDevice.Instance.CreateTexture(); } // Set timeout to remove old buffer _oldBackbufferLiveTimeLeft = 3; // Resize backbuffer var desc = GPUTextureDescription.New2D(width, height, BackBufferFormat); _backBuffer.Init(ref desc); _task.Output = _backBuffer; } /// public override void OnDestroy() { if (IsDisposing) return; // Cleanup Scripting.Update -= OnUpdate; if (_task != null) { _task.Enabled = false; //_task.CustomPostFx.Clear(); } Object.Destroy(ref _backBuffer); Object.Destroy(ref _backBufferOld); Object.Destroy(ref _task); base.OnDestroy(); } } }