Files
2026-07-13 13:13:17 +08:00

459 lines
17 KiB
C#

using System.Windows.Forms;
using SharpDX;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using T3.Core.IO;
using T3.Core.Resource;
using T3.Core.SystemUi;
using T3.Editor.Gui;
// for ReleaseMode
using T3.Editor.Gui.UiHelpers;
using T3.Editor.UiModel;
using Device = SharpDX.Direct3D11.Device;
using PixelShader = T3.Core.DataTypes.PixelShader;
using VertexShader = T3.Core.DataTypes.VertexShader;
using Vector2 = System.Numerics.Vector2;
namespace T3.Editor.App;
internal static class ProgramWindows
{
public static AppWindow Main { get; private set; }
public static AppWindow Viewer { get; private set; } // Required it distinguish 2nd render view in mouse handling
private static Device _device;
private static DeviceContext _deviceContext;
private static Factory _factory;
public static string ActiveGpu { get; private set; } = "Unknown";
internal static void SetMainWindowSize(int width, int height)
{
Main.SetSize(width, height);
Main.SetBorderStyleSizable();
}
internal static void SetInteractionDevices(params object[] objects)
{
IWindowsFormsMessageHandler[] messageHandlers = objects.OfType<IWindowsFormsMessageHandler>().ToArray();
ImGuiDx11RenderForm.InputMethods = messageHandlers;
}
public static void SetVertexShader(Resource<VertexShader> resource) => _deviceContext.VertexShader.Set(resource.Value);
public static void SetPixelShader(Resource<PixelShader> resource) => _deviceContext.PixelShader.Set(resource.Value);
internal static void HandleFullscreenToggle()
{
if (Main.IsFullScreen == UserSettings.Config.FullScreen)
return;
var screenCount = Screen.AllScreens.Length;
if (UserSettings.Config.FullScreen)
{
Main.SetFullScreen(UserSettings.Config.FullScreenIndexMain < screenCount ? UserSettings.Config.FullScreenIndexMain : 0);
}
else
{
Main.SetSizeable();
}
}
/// <summary>
/// Updates the viewer window spanning bounds dynamically
/// Called whenever the spanning area selection changes in the Screen Manager
/// </summary>
internal static void UpdateViewerSpanning(ImRect spanningBounds)
{
if (Viewer == null)
return;
// Check if there's a valid spanning area defined
if (spanningBounds.Max.X > 0 && spanningBounds.Max.Y > 0)
{
// Update the viewer window to the spanning bounds
Viewer.UpdateSpanningBounds(
(int)spanningBounds.Min.X,
(int)spanningBounds.Min.Y,
(int)spanningBounds.Max.X,
(int)spanningBounds.Max.Y
);
}
}
/// <summary>
/// Call this when the secondary render window is enabled/disabled
/// to ensure the viewer window is properly configured
/// </summary>
internal static void UpdateViewerWindowState()
{
if (Viewer == null)
return;
var spanning = UserSettings.Config.OutputArea;
var spanningBounds = new ImRect(new Vector2(spanning.X, spanning.Y), new Vector2(spanning.Z, spanning.W));
var isSpanningValid = spanningBounds.Max.X > 0 && spanningBounds.Max.Y > 0;
if (isSpanningValid)
{
UpdateViewerSpanning(spanningBounds);
}
}
private sealed class DisplayAdapterRating()
{
public string Name;
public int Index;
public float MemoryInGb =0;
public float Rating = 1;
}
internal static void InitializeMainWindow(string version, out Device device)
{
Main = new AppWindow("TiXL " + version, disableClose: false);
// Enable explicit frame-latency pacing on Main's swap chain. Replaces the accidental
// pacing previously provided by Viewer's secondary `Present(1)` call. Must be set before
// SwapChainDescription is consumed by Device.CreateWithSwapChain.
Main.UseFrameLatencyWaitable = true;
device = null;
string[] highPerformanceKeywords = ["dedicated", "high performance", "rtx", "gtx"];
string[] integratedKeywords = ["integrated", "intel(r) uhd graphics", "microsoft basic render", "microsoft basic render"]; // twice to make MS worse
try
{
using var factory = new Factory1();
if (factory.GetAdapterCount() == 0)
{
BlockingWindow.Instance.ShowMessageBox("We are unable to find any graphics adapters",
"Oh noooo",
"OK");
Environment.Exit(0);
}
var adapterRatings = new List<DisplayAdapterRating>(8);
for (var i = 0; i < factory.GetAdapterCount(); i++)
{
using var adapter = factory.GetAdapter1(i);
const long gb = 1024 * 1024 * 1024;
var newRating = new DisplayAdapterRating
{
Name = adapter.Description.Description,
Index = i,
MemoryInGb = (float)((double)adapter.Description.DedicatedVideoMemory/gb),
};
adapterRatings.Add(newRating);
var descriptionLower = adapter.Description.Description.ToLowerInvariant();
// Positive keywords
foreach (var keyword in highPerformanceKeywords)
{
if (!descriptionLower.Contains(keyword))
continue;
newRating.Rating *= 2f;
}
// Negative keywords
foreach (var keyword in integratedKeywords)
{
if (!descriptionLower.Contains(keyword))
continue;
newRating.Rating *= 0.2f;
}
var memSizeFactor = newRating.MemoryInGb switch
{
< 1 => 0.1f,
< 2 => 0.5f,
< 4 => 1f,
< 8 => 2f,
> 8 => 3f,
_ => 4f
};
newRating.Rating *= memSizeFactor;
}
var selectedAdapterIndex = adapterRatings.OrderByDescending(r => r.Rating).First().Index;
Log.Debug("Detected display adapters...");
foreach (var r in adapterRatings)
{
Log.Debug($" #{r.Index}: {r.Name} / {r.MemoryInGb:0.0}GB -> rated {r.Rating:0.0}");
}
var selectedAdapter = factory.GetAdapter1(selectedAdapterIndex);
ActiveGpu = selectedAdapter.Description.Description;
//Try to load 11.1 if possible, revert to 11.0 auto
FeatureLevel[] levels =
[
FeatureLevel.Level_11_1,
FeatureLevel.Level_11_0,
];
// Create Device and SwapChain with the selected adapter
var deviceCreationFlags = DeviceCreationFlags.BgraSupport;
if (CoreSettings.Config.EnableDirectXDebug)
deviceCreationFlags |= DeviceCreationFlags.Debug;
Log.Debug("Creating Device...");
SwapChain swapchain;
try
{
Device.CreateWithSwapChain(selectedAdapter,
deviceCreationFlags,
levels,
Main.SwapChainDescription,
out device,
out swapchain);
}
catch (Exception e)
{
Log.Warning("Failed to create device with advanced features. Trying basic settings. " + e.Message);
Device.CreateWithSwapChain(selectedAdapter,
DeviceCreationFlags.None,
levels,
Main.SwapChainDescription,
out device,
out swapchain);
}
_device = device;
_deviceContext = device.ImmediateContext;
_factory = swapchain.GetParent<Factory>();
Main.SetDevice(device, _deviceContext, swapchain);
Main.InitializeWindow(FormWindowState.Maximized, OnCloseMainWindow, true);
_factory.MakeWindowAssociation(Main.HwndHandle, WindowAssociationFlags.IgnoreAll);
}
catch (Exception e)
{
if (e.Message.Contains("DXGI_ERROR_SDK_COMPONENT_MISSING"))
{
var result =
BlockingWindow.Instance
.ShowMessageBox("You need to install the Windows Graphics Diagnostics Tools.\n\nClick OK to download this Windows component directly from Microsoft.",
"Windows Component Missing", "OK", "Cancel");
if (result == "Ok")
{
CoreUi.Instance
.OpenWithDefaultApplication("https://learn.microsoft.com/en-us/windows/uwp/gaming/use-the-directx-runtime-and-visual-studio-graphics-diagnostic-features");
}
}
else
{
BlockingWindow.Instance.ShowMessageBox("We are sorry but your graphics hardware might not be capable of running TiXL\n\n" + e.Message,
"Oh noooo",
"Ok... /:");
}
Environment.Exit(0);
}
}
internal static void InitializeSecondaryViewerWindow(string name, int width, int height)
{
Viewer = new(name, disableClose: true);
Viewer.SetDevice(_device, _deviceContext);
Viewer.SetSize(width, height);
Viewer.SetSizeable();
Viewer.InitViewSwapChain(_factory);
Viewer.InitializeWindow(FormWindowState.Normal, null, false);
Viewer.Show();
}
private static void OnCloseMainWindow(object sender, FormClosingEventArgs args)
{
if (EditableSymbolProject.AllProjects.Any(x => x.IsSaving))
{
args.Cancel = true;
Log.Debug($"Cancel closing because save-operation is in progress.");
}
else
{
#if DEBUG
args.Cancel = false;
#else
args.Cancel = true;
T3Ui.ExitDialog.ShowNextFrame();
#endif
}
}
public static void Release()
{
Main.Release();
Viewer.Release();
_device.ImmediateContext.ClearState();
_deviceContext.Flush();
_device.Dispose();
_deviceContext.Dispose();
_factory.Dispose();
}
public static void SetRasterizerState(RasterizerState viewWindowRasterizerState)
{
_deviceContext.Rasterizer.State = viewWindowRasterizerState;
}
public static void SetPixelShaderSRV(ShaderResourceView viewWindowBackgroundSrv)
{
_deviceContext.PixelShader.SetShaderResource(0, viewWindowBackgroundSrv);
}
public static void DrawTextureToSecondaryRenderOutput()
{
_deviceContext.Draw(3, 0);
_deviceContext.PixelShader.SetShaderResource(0, null);
}
public static void RefreshViewport()
{
_deviceContext.Rasterizer.SetViewport(new Viewport(0, 0, Main.Width, Main.Height, 0.0f, 1.0f));
_deviceContext.OutputMerger.SetTargets(Main.RenderTargetView);
}
public static void Present(bool useVSync, bool showSecondaryRenderWindow)
{
try
{
Main.SwapChain.Present(useVSync ? 1 : 0, PresentFlags.None);
// Always present the Viewer's swap chain, regardless of whether its window is shown.
// Empirically, having two flip-model Present calls per frame in the same process
// gives DWM's scheduler a noticeably better composition slot for Main — the Viewer's
// Present acts as a co-pacing primitive even when its window is hidden. The cost of
// the extra Present is small (the back buffer is unchanged when ShowSecondaryRenderWindow
// is false; DWM doesn't display the hidden window; FlipDiscard discards the buffer
// immediately on the next present cycle).
Viewer?.SwapChain?.Present(useVSync ? 1 : 0, PresentFlags.None);
}
catch (SharpDX.SharpDXException e)
{
string description;
var result = _device.DeviceRemovedReason;
if (result == Result.Abort)
{
description = Result.Abort.Description;
}
else if (result == Result.AccessDenied)
{
description = Result.AccessDenied.Description;
}
else if (result == Result.Fail)
{
description = Result.Fail.Description;
}
else if (result == Result.Handle)
{
description = Result.Handle.Description;
}
else if (result == Result.InvalidArg)
{
description = Result.InvalidArg.Description;
}
else if (result == Result.NoInterface)
{
description = Result.NoInterface.Description;
}
else if (result == Result.NotImplemented)
{
description = Result.NotImplemented.Description;
}
else if (result == Result.OutOfMemory)
{
description = Result.OutOfMemory.Description;
}
else if (result == Result.InvalidPointer)
{
description = Result.InvalidPointer.Description;
}
else if (result == Result.UnexpectedFailure)
{
description = Result.UnexpectedFailure.Description;
}
else if (result == Result.WaitAbandoned)
{
description = Result.WaitAbandoned.Description;
}
else if (result == Result.WaitTimeout)
{
description = Result.WaitTimeout.Description;
}
else if (result == Result.Pending)
{
description = Result.Pending.Description;
}
else
{
description = "unknown reason";
}
var resultCode = result.ToString();
throw (new ApplicationException($"Graphics card suspended ({resultCode}: {description}): {e.Message}"));
}
}
public static void RebuildUiCopyTextureIfRequired()
{
var needsRebuild = _uiCopyTexture == null ||
_uiCopyTexture.Description.Width != Main.SwapChain.Description.ModeDescription.Width ||
_uiCopyTexture.Description.Height != Main.SwapChain.Description.ModeDescription.Height;
if (!needsRebuild)
return;
// Create a shader resource-compatible texture
var textureDesc = new Texture2DDescription
{
Width = Main.SwapChain.Description.ModeDescription.Width,
Height = Main.SwapChain.Description.ModeDescription.Height,
MipLevels = 1,
ArraySize = 1,
Format = Main.SwapChain.Description.ModeDescription.Format,
SampleDescription = new SampleDescription(1, 0),
Usage = ResourceUsage.Default,
BindFlags = BindFlags.ShaderResource,
CpuAccessFlags = CpuAccessFlags.None,
OptionFlags = ResourceOptionFlags.None
};
if (_uiCopyTexture is { IsDisposed: false })
_uiCopyTexture.Dispose();
_uiCopyTexture = new Texture2D(_device, textureDesc);
if (UiCopyTextureSrv is { IsDisposed: false })
UiCopyTextureSrv.Dispose();
UiCopyTextureSrv = new ShaderResourceView(_device, _uiCopyTexture);
}
/// <summary>
/// For things like presentations, demos or certain live performance situations it
/// can be desired to share also T3's UI content on a second display.
///
/// On Windows duplicating a display is extremely expensive. This work around
/// copies the last frame into a texture which is then presented on the second display.
/// </summary>
public static void CopyUiContentToShareTexture()
{
if (_uiCopyTexture == null || _uiCopyTexture.IsDisposed)
{
Log.Warning("Can't use undefined uiCopyTexture");
return;
}
_deviceContext.CopyResource(Main.BackBufferTexture, _uiCopyTexture);
}
private static Texture2D _uiCopyTexture;
public static ShaderResourceView UiCopyTextureSrv { get; private set; }
}