// Copyright (c) 2012-2024 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 or sets a value indicating whether render to that output only if parent window exists, otherwise false.
///
public bool RenderOnlyWithWindow { get; set; } = true;
///
/// Gets or sets a value indicating whether use automatic task rendering skipping if output is too small or window is missing. Disable it to manually control .
///
public bool UseAutomaticTaskManagement { 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 used 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)
{
_task = task ?? throw new ArgumentNullException();
_backBuffer = GPUDevice.Instance.CreateTexture("RenderOutputControl.BackBuffer");
_resizeTime = ResizeCheckTime;
_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 || !UseAutomaticTaskManagement)
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(Float2.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 * 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("RenderOutputControl.BackBuffer");
}
// 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.ClearCustomActors();
//_task.CustomPostFx.Clear();
}
Object.Destroy(ref _backBuffer);
Object.Destroy(ref _backBufferOld);
Object.Destroy(ref _task);
base.OnDestroy();
}
}
}