From 1abaeef5ab183efb225cda4a62a252936f484555 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 18:54:02 +0200 Subject: [PATCH 01/71] feat: use WorkingPluginId as identifier for plugins to load from profiles --- .../PluginInstaller/PluginInstallerWindow.cs | 24 ++++----- .../PluginInstaller/ProfileManagerWidget.cs | 16 +++--- Dalamud/Plugin/Internal/PluginManager.cs | 47 +++++++++------- Dalamud/Plugin/Internal/Profiles/Profile.cs | 54 +++++++++++++------ .../Internal/Profiles/ProfileManager.cs | 42 ++++++++++----- .../Plugin/Internal/Profiles/ProfileModel.cs | 36 +++++++++++-- .../Internal/Profiles/ProfileModelV1.cs | 2 + .../Internal/Profiles/ProfilePluginEntry.cs | 5 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- 9 files changed, 155 insertions(+), 73 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index dcbdced28..163e62b78 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2381,10 +2381,10 @@ internal class PluginInstallerWindow : Window, IDisposable var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev; var profilesThatWantThisPlugin = profileManager.Profiles - .Where(x => x.WantsPlugin(plugin.InternalName) != null) + .Where(x => x.WantsPlugin(plugin.Manifest.WorkingPluginId) != null) .ToArray(); var isInSingleProfile = profilesThatWantThisPlugin.Length == 1; - var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName); + var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.WorkingPluginId); // Disable everything if the updater is running or another plugin is operating var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress; @@ -2419,17 +2419,17 @@ internal class PluginInstallerWindow : Window, IDisposable foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile)) { - var inProfile = profile.WantsPlugin(plugin.Manifest.InternalName) != null; + var inProfile = profile.WantsPlugin(plugin.Manifest.WorkingPluginId) != null; if (ImGui.Checkbox($"###profilePick{profile.Guid}{plugin.Manifest.InternalName}", ref inProfile)) { if (inProfile) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd); } else { - Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName)) + Task.Run(() => profile.RemoveAsync(plugin.Manifest.WorkingPluginId)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotRemove); } } @@ -2449,11 +2449,11 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { // TODO: Work this out - Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, plugin.IsLoaded, false)) + Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.IsLoaded, false)) .GetAwaiter().GetResult(); foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName))) { - Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName, false)) + Task.Run(() => profile.RemoveAsync(plugin.Manifest.WorkingPluginId, false)) .GetAwaiter().GetResult(); } @@ -2527,7 +2527,7 @@ internal class PluginInstallerWindow : Window, IDisposable { await plugin.UnloadAsync(); await applicableProfile.AddOrUpdateAsync( - plugin.Manifest.InternalName, false, false); + plugin.Manifest.WorkingPluginId, false, false); notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success); }).ContinueWith(t => @@ -2544,7 +2544,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle; this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId; - await applicableProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); await plugin.LoadAsync(PluginLoadReason.Installer); notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success); @@ -2565,7 +2565,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (shouldUpdate) { // We need to update the profile right here, because PM will not enable the plugin otherwise - await applicableProfile.AddOrUpdateAsync(plugin.InternalName, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); await this.UpdateSinglePlugin(availableUpdate); } else @@ -2739,7 +2739,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (localPlugin is LocalDevPlugin plugin) { var isInDefaultProfile = - Service.Get().IsInDefaultProfile(localPlugin.Manifest.InternalName); + Service.Get().IsInDefaultProfile(localPlugin.Manifest.WorkingPluginId); // https://colorswall.com/palette/2868/ var greenColor = new Vector4(0x5C, 0xB8, 0x5C, 0xFF) / 0xFF; @@ -3083,7 +3083,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.pluginListAvailable.Sort((p1, p2) => p1.Name.CompareTo(p2.Name)); var profman = Service.Get(); - this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.InternalName).CompareTo(profman.IsInDefaultProfile(p2.InternalName))); + this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.Manifest.WorkingPluginId).CompareTo(profman.IsInDefaultProfile(p2.Manifest.WorkingPluginId))); break; default: throw new InvalidEnumArgumentException("Unknown plugin sort type."); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 039877158..2be074f84 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -229,7 +229,7 @@ internal class ProfileManagerWidget if (ImGuiComponents.IconButton($"###exportButton{profile.Guid}", FontAwesomeIcon.FileExport)) { - ImGui.SetClipboardText(profile.Model.Serialize()); + ImGui.SetClipboardText(profile.Model.SerializeForShare()); Service.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success); } @@ -300,7 +300,7 @@ internal class ProfileManagerWidget if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}")) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } } @@ -327,7 +327,7 @@ internal class ProfileManagerWidget if (ImGuiComponents.IconButton(FontAwesomeIcon.FileExport)) { - ImGui.SetClipboardText(profile.Model.Serialize()); + ImGui.SetClipboardText(profile.Model.SerializeForShare()); Service.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success); } @@ -400,7 +400,7 @@ internal class ProfileManagerWidget if (pluginListChild) { var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale; - string? wantRemovePluginInternalName = null; + Guid? wantRemovePluginGuid = null; using var syncScope = profile.GetSyncScope(); foreach (var plugin in profile.Plugins.ToArray()) @@ -467,7 +467,7 @@ internal class ProfileManagerWidget var enabled = plugin.IsEnabled; if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled)) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.InternalName, enabled)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.WorkingPluginId, enabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } @@ -477,17 +477,17 @@ internal class ProfileManagerWidget if (ImGuiComponents.IconButton($"###removePlugin{plugin.InternalName}", FontAwesomeIcon.Trash)) { - wantRemovePluginInternalName = plugin.InternalName; + wantRemovePluginGuid = plugin.WorkingPluginId; } if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.RemovePlugin); } - if (wantRemovePluginInternalName != null) + if (wantRemovePluginGuid != null) { // TODO: handle error - Task.Run(() => profile.RemoveAsync(wantRemovePluginInternalName, false)) + Task.Run(() => profile.RemoveAsync(wantRemovePluginGuid.Value, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotRemove); } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 691d5f729..f782b4129 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1290,13 +1290,27 @@ internal partial class PluginManager : IDisposable, IServiceType if (isDev) { Log.Information($"Loading dev plugin {name}"); - var devPlugin = new LocalDevPlugin(dllFile, manifest); + plugin = new LocalDevPlugin(dllFile, manifest); + } + else + { + Log.Information($"Loading plugin {name}"); + plugin = new LocalPlugin(dllFile, manifest); + } + + // Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here. + if (plugin.Manifest.WorkingPluginId == Guid.Empty) + throw new Exception("Plugin should have a WorkingPluginId at this point"); + this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.Manifest.WorkingPluginId); + + // Now, if this is a devPlugin, figure out if we want to load it + if (isDev) + { + var devPlugin = (LocalDevPlugin)plugin; loadPlugin &= !isBoot; - var probablyInternalNameForThisPurpose = manifest?.InternalName ?? dllFile.Name; - var wantsInDefaultProfile = - this.profileManager.DefaultProfile.WantsPlugin(probablyInternalNameForThisPurpose); + this.profileManager.DefaultProfile.WantsPlugin(plugin.Manifest.WorkingPluginId); if (wantsInDefaultProfile == null) { // We don't know about this plugin, so we don't want to do anything here. @@ -1305,46 +1319,41 @@ internal partial class PluginManager : IDisposable, IServiceType else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot) { // We didn't want this plugin, and StartOnBoot is on. That means we don't want it and it should stay off until manually enabled. - Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); + Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot) { // We wanted this plugin, and StartOnBoot is on. That means we actually do want it. - Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, true, false); + Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); loadPlugin = !doNotLoad; } else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot) { // We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore. - Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); + Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot) { // We didn't want this plugin, and StartOnBoot is off. We don't want it. - Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); + Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); loadPlugin = false; } plugin = devPlugin; } - else - { - Log.Information($"Loading plugin {name}"); - plugin = new LocalPlugin(dllFile, manifest); - } #pragma warning disable CS0618 var defaultState = manifest?.Disabled != true && loadPlugin; #pragma warning restore CS0618 - + // Need to do this here, so plugins that don't load are still added to the default profile - var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.InternalName, defaultState); + var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, defaultState); if (loadPlugin) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index ac46d9153..657cde534 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -102,7 +102,7 @@ internal class Profile /// Gets all plugins declared in this profile. /// public IEnumerable Plugins => - this.modelV1.Plugins.Select(x => new ProfilePluginEntry(x.InternalName, x.IsEnabled)); + this.modelV1.Plugins.Select(x => new ProfilePluginEntry(x.InternalName, x.WorkingPluginId, x.IsEnabled)); /// /// Gets this profile's underlying model. @@ -144,11 +144,11 @@ internal class Profile /// /// The internal name of the plugin. /// Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled. - public bool? WantsPlugin(string internalName) + public bool? WantsPlugin(Guid workingPluginId) { lock (this) { - var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + var entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); return entry?.IsEnabled; } } @@ -161,13 +161,13 @@ internal class Profile /// Whether or not the plugin should be enabled. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. - public async Task AddOrUpdateAsync(string internalName, bool state, bool apply = true) + public async Task AddOrUpdateAsync(Guid workingPluginId, bool state, bool apply = true) { - Debug.Assert(!internalName.IsNullOrEmpty(), "!internalName.IsNullOrEmpty()"); - + Debug.Assert(workingPluginId != Guid.Empty, "Trying to add plugin with empty guid"); + lock (this) { - var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + var existing = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); if (existing != null) { existing.IsEnabled = state; @@ -176,16 +176,16 @@ internal class Profile { this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin { - InternalName = internalName, + WorkingPluginId = workingPluginId, IsEnabled = state, }); } } // We need to remove this plugin from the default profile, if it declares it. - if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(internalName) != null) + if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(workingPluginId) != null) { - await this.manager.DefaultProfile.RemoveAsync(internalName, false); + await this.manager.DefaultProfile.RemoveAsync(workingPluginId, false); } Service.Get().QueueSave(); @@ -201,25 +201,25 @@ internal class Profile /// The internal name of the plugin. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. - public async Task RemoveAsync(string internalName, bool apply = true) + public async Task RemoveAsync(Guid workingPluginId, bool apply = true) { ProfileModelV1.ProfileModelV1Plugin entry; lock (this) { - entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); if (entry == null) - throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\""); + throw new ArgumentException($"No plugin \"{workingPluginId}\" in profile \"{this.Guid}\""); if (!this.modelV1.Plugins.Remove(entry)) throw new Exception("Couldn't remove plugin from model collection"); } // We need to add this plugin back to the default profile, if we were the last profile to have it. - if (!this.manager.IsInAnyProfile(internalName)) + if (!this.manager.IsInAnyProfile(workingPluginId)) { if (!this.IsDefaultProfile) { - await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, this.IsEnabled && entry.IsEnabled, false); + await this.manager.DefaultProfile.AddOrUpdateAsync(workingPluginId, this.IsEnabled && entry.IsEnabled, false); } else { @@ -233,6 +233,30 @@ internal class Profile await this.manager.ApplyAllWantStatesAsync(); } + /// + /// This function tries to migrate all plugins with this internalName which do not have + /// a GUID to the specified GUID. + /// This is best-effort and will probably work well for anyone that only uses regular plugins. + /// + /// InternalName of the plugin to migrate. + /// Guid to use. + public void MigrateProfilesToGuidsForPlugin(string internalName, Guid newGuid) + { + lock (this) + { + foreach (var plugin in this.modelV1.Plugins) + { + if (plugin.InternalName == internalName && plugin.WorkingPluginId == Guid.Empty) + { + plugin.WorkingPluginId = newGuid; + Log.Information("Migrated profile {Profile} plugin {Name} to guid {Guid}", this, internalName, newGuid); + } + } + } + + Service.Get().QueueSave(); + } + /// public override string ToString() => $"{this.Guid} ({this.Name})"; } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 46b572c1a..1d14ade4b 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -73,7 +73,7 @@ internal class ProfileManager : IServiceType /// The state the plugin shall be in, if it needs to be added. /// Whether or not the plugin should be added to the default preset, if it's not present in any preset. /// Whether or not the plugin shall be enabled. - public async Task GetWantStateAsync(string internalName, bool defaultState, bool addIfNotDeclared = true) + public async Task GetWantStateAsync(Guid workingPluginId, bool defaultState, bool addIfNotDeclared = true) { var want = false; var wasInAnyProfile = false; @@ -82,7 +82,7 @@ internal class ProfileManager : IServiceType { foreach (var profile in this.profiles) { - var state = profile.WantsPlugin(internalName); + var state = profile.WantsPlugin(workingPluginId); if (state.HasValue) { want = want || (profile.IsEnabled && state.Value); @@ -93,8 +93,8 @@ internal class ProfileManager : IServiceType if (!wasInAnyProfile && addIfNotDeclared) { - Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState); - await this.DefaultProfile.AddOrUpdateAsync(internalName, defaultState, false); + Log.Warning("{Guid} was not in any profile, adding to default with {Default}", workingPluginId, defaultState); + await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, defaultState, false); return defaultState; } @@ -107,10 +107,10 @@ internal class ProfileManager : IServiceType /// /// The internal name of the plugin. /// Whether or not the plugin is in any profile. - public bool IsInAnyProfile(string internalName) + public bool IsInAnyProfile(Guid workingPluginId) { lock (this.profiles) - return this.profiles.Any(x => x.WantsPlugin(internalName) != null); + return this.profiles.Any(x => x.WantsPlugin(workingPluginId) != null); } /// @@ -119,8 +119,8 @@ internal class ProfileManager : IServiceType /// /// The internal name of the plugin. /// Whether or not the plugin is in the default profile. - public bool IsInDefaultProfile(string internalName) - => this.DefaultProfile.WantsPlugin(internalName) != null; + public bool IsInDefaultProfile(Guid workingPluginId) + => this.DefaultProfile.WantsPlugin(workingPluginId) != null; /// /// Add a new profile. @@ -151,7 +151,7 @@ internal class ProfileManager : IServiceType /// The newly cloned profile. public Profile CloneProfile(Profile toClone) { - var newProfile = this.ImportProfile(toClone.Model.Serialize()); + var newProfile = this.ImportProfile(toClone.Model.SerializeForShare()); if (newProfile == null) throw new Exception("New profile was null while cloning"); @@ -196,13 +196,13 @@ internal class ProfileManager : IServiceType this.isBusy = true; Log.Information("Getting want states..."); - List wantActive; + List wantActive; lock (this.profiles) { wantActive = this.profiles .Where(x => x.IsEnabled) .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled) - .Select(plugin => plugin.InternalName)) + .Select(plugin => plugin.WorkingPluginId)) .Distinct().ToList(); } @@ -218,7 +218,7 @@ internal class ProfileManager : IServiceType var pm = Service.Get(); foreach (var installedPlugin in pm.InstalledPlugins) { - var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName); + var wantThis = wantActive.Contains(installedPlugin.Manifest.WorkingPluginId); switch (wantThis) { case true when !installedPlugin.IsLoaded: @@ -267,7 +267,7 @@ internal class ProfileManager : IServiceType // We need to remove all plugins from the profile first, so that they are re-added to the default profile if needed foreach (var plugin in profile.Plugins.ToArray()) { - await profile.RemoveAsync(plugin.InternalName, false); + await profile.RemoveAsync(plugin.WorkingPluginId, false); } if (!this.config.SavedProfiles!.Remove(profile.Model)) @@ -279,6 +279,22 @@ internal class ProfileManager : IServiceType this.config.QueueSave(); } + /// + /// This function tries to migrate all plugins with this internalName which do not have + /// a GUID to the specified GUID. + /// This is best-effort and will probably work well for anyone that only uses regular plugins. + /// + /// InternalName of the plugin to migrate. + /// Guid to use. + public void MigrateProfilesToGuidsForPlugin(string internalName, Guid newGuid) + { + lock (this.profiles) + { + foreach (var profile in this.profiles) + profile.MigrateProfilesToGuidsForPlugin(internalName, newGuid); + } + } + private string GenerateUniqueProfileName(string startingWith) { if (this.profiles.All(x => x.Name != startingWith)) diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs index bf2a9c2c9..d77cab443 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs @@ -1,7 +1,9 @@ using System; - +using System.Collections.Generic; +using System.Reflection; using Dalamud.Utility; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace Dalamud.Plugin.Internal.Profiles; @@ -39,11 +41,11 @@ public abstract class ProfileModel } /// - /// Serialize this model into a string usable for sharing. + /// Serialize this model into a string usable for sharing, without including GUIDs. /// /// The serialized representation of the model. /// Thrown when an unsupported model is serialized. - public string Serialize() + public string SerializeForShare() { string prefix; switch (this) @@ -55,6 +57,32 @@ public abstract class ProfileModel throw new ArgumentOutOfRangeException(); } - return prefix + Convert.ToBase64String(Util.CompressString(JsonConvert.SerializeObject(this))); + // HACK: Just filter the ID for now, we should split the sharing + saving model + var serialized = JsonConvert.SerializeObject(this, new JsonSerializerSettings() + { ContractResolver = new IgnorePropertiesResolver(new[] { "WorkingPluginId" }) }); + + return prefix + Convert.ToBase64String(Util.CompressString(serialized)); + } + + // Short helper class to ignore some properties from serialization + private class IgnorePropertiesResolver : DefaultContractResolver + { + private readonly HashSet ignoreProps; + + public IgnorePropertiesResolver(IEnumerable propNamesToIgnore) + { + this.ignoreProps = new HashSet(propNamesToIgnore); + } + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + if (this.ignoreProps.Contains(property.PropertyName)) + { + property.ShouldSerialize = _ => false; + } + + return property; + } } } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs index 2a851d234..1b224c8dc 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs @@ -46,6 +46,8 @@ public class ProfileModelV1 : ProfileModel /// Gets or sets the internal name of the plugin. /// public string? InternalName { get; set; } + + public Guid WorkingPluginId { get; set; } /// /// Gets or sets a value indicating whether or not this entry is enabled. diff --git a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs index 0a6f5140b..2c10def99 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs @@ -10,9 +10,10 @@ internal class ProfilePluginEntry /// /// The internal name of the plugin. /// A value indicating whether or not this entry is enabled. - public ProfilePluginEntry(string internalName, bool state) + public ProfilePluginEntry(string internalName, Guid workingPluginId, bool state) { this.InternalName = internalName; + this.WorkingPluginId = workingPluginId; this.IsEnabled = state; } @@ -20,6 +21,8 @@ internal class ProfilePluginEntry /// Gets the internal name of the plugin. /// public string InternalName { get; } + + public Guid WorkingPluginId { get; set; } /// /// Gets a value indicating whether or not this entry is enabled. diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index f7306b5a7..8abfd2f9f 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -235,7 +235,7 @@ internal class LocalPlugin : IDisposable /// INCLUDES the default profile. /// public bool IsWantedByAnyProfile => - Service.Get().GetWantStateAsync(this.manifest.InternalName, false, false).GetAwaiter().GetResult(); + Service.Get().GetWantStateAsync(this.manifest.WorkingPluginId, false, false).GetAwaiter().GetResult(); /// /// Gets a value indicating whether this plugin's API level is out of date. From a9f6d6d104080148068c2b422968135ea439ce47 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:04:08 +0200 Subject: [PATCH 02/71] chore: ModuleLog fmt objects may be nullable --- Dalamud/Logging/Internal/ModuleLog.cs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index 2fb735640..baa2708ac 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -33,7 +33,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Verbose(string messageTemplate, params object[] values) + public void Verbose(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, null, values); /// @@ -42,7 +42,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Verbose(Exception exception, string messageTemplate, params object[] values) + public void Verbose(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, exception, values); /// @@ -50,7 +50,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Debug(string messageTemplate, params object[] values) + public void Debug(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, null, values); /// @@ -59,7 +59,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Debug(Exception exception, string messageTemplate, params object[] values) + public void Debug(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, exception, values); /// @@ -67,7 +67,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Information(string messageTemplate, params object[] values) + public void Information(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, null, values); /// @@ -76,7 +76,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Information(Exception exception, string messageTemplate, params object[] values) + public void Information(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, exception, values); /// @@ -84,7 +84,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Warning(string messageTemplate, params object[] values) + public void Warning(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, null, values); /// @@ -93,7 +93,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Warning(Exception exception, string messageTemplate, params object[] values) + public void Warning(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, exception, values); /// @@ -101,7 +101,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Error(string messageTemplate, params object[] values) + public void Error(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, null, values); /// @@ -110,7 +110,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Error(Exception? exception, string messageTemplate, params object[] values) + public void Error(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, exception, values); /// @@ -118,7 +118,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Fatal(string messageTemplate, params object[] values) + public void Fatal(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, null, values); /// @@ -127,11 +127,11 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Fatal(Exception exception, string messageTemplate, params object[] values) + public void Fatal(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); private void WriteLog( - LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) + LogEventLevel level, string messageTemplate, Exception? exception = null, params object?[] values) { // FIXME: Eventually, the `pluginName` tag should be removed from here and moved over to the actual log // formatter. From 6e54c085fa32a14a11699f6cea86b114f81c8394 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:08:58 +0200 Subject: [PATCH 03/71] fix: find matching plugins when importing a profile --- Dalamud/Plugin/Internal/PluginManager.cs | 3 +++ Dalamud/Plugin/Internal/Profiles/Profile.cs | 4 ++++ .../Internal/Profiles/ProfileManager.cs | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index f782b4129..37dab0f03 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1299,6 +1299,9 @@ internal partial class PluginManager : IDisposable, IServiceType } // Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here. + // This will also happen if you are installing a plugin with the installer, and that's intended! + // It means that, if you have a profile which has unsatisfied plugins, installing a matching plugin will + // enter it into the profiles it can match. if (plugin.Manifest.WorkingPluginId == Guid.Empty) throw new Exception("Plugin should have a WorkingPluginId at this point"); this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.Manifest.WorkingPluginId); diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 657cde534..b9c90235a 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -246,6 +246,10 @@ internal class Profile { foreach (var plugin in this.modelV1.Plugins) { + // TODO: What should happen if a profile has a GUID locked in, but the plugin + // is not installed anymore? That probably means that the user uninstalled the plugin + // and is now reinstalling it. We should still satisfy that and update the ID. + if (plugin.InternalName == internalName && plugin.WorkingPluginId == Guid.Empty) { plugin.WorkingPluginId = newGuid; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 1d14ade4b..d8f091e9f 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -172,7 +172,27 @@ internal class ProfileManager : IServiceType newModel.Guid = Guid.NewGuid(); newModel.Name = this.GenerateUniqueProfileName(newModel.Name.IsNullOrEmpty() ? "Unknown Collection" : newModel.Name); if (newModel is ProfileModelV1 modelV1) + { + // Disable it modelV1.IsEnabled = false; + + // Try to find matching plugins for all plugins in the profile + var pm = Service.Get(); + foreach (var plugin in modelV1.Plugins) + { + var installedPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName); + if (installedPlugin != null) + { + Log.Information("Satisfying plugin {InternalName} for profile {Name} with {Guid}", plugin.InternalName, newModel.Name, installedPlugin.Manifest.WorkingPluginId); + plugin.WorkingPluginId = installedPlugin.Manifest.WorkingPluginId; + } + else + { + Log.Warning("Couldn't find plugin {InternalName} for profile {Name}", plugin.InternalName, newModel.Name); + plugin.WorkingPluginId = Guid.Empty; + } + } + } this.config.SavedProfiles!.Add(newModel); this.config.QueueSave(); From 8b85139e6198ad22257aa1938d866c8e79a262a7 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:16:54 +0200 Subject: [PATCH 04/71] chore: prevent plugins from being installed twice for now if an assignment is missing --- .../PluginInstaller/ProfileManagerWidget.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 2be074f84..7c9026505 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -406,7 +406,7 @@ internal class ProfileManagerWidget foreach (var plugin in profile.Plugins.ToArray()) { didAny = true; - var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName); + var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.WorkingPluginId == plugin.WorkingPluginId); var btnOffset = 2; if (pmPlugin != null) @@ -437,26 +437,33 @@ internal class ProfileManagerWidget ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); ImGui.TextUnformatted(text); - - var available = + + var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == plugin.InternalName); + var installable = pm.AvailablePlugins.FirstOrDefault( x => x.InternalName == plugin.InternalName && !x.SourceRepo.IsThirdParty); - if (available != null) + + if (firstAvailableInstalled != null) + { + // TODO + ImGui.Text("GOAT WAS TOO LAZY TO IMPLEMENT THIS"); + } + else if (installable != null) { ImGui.SameLine(); ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * 2) - 2); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); btnOffset = 3; - if (ImGuiComponents.IconButton($"###installMissingPlugin{available.InternalName}", FontAwesomeIcon.Download)) + if (ImGuiComponents.IconButton($"###installMissingPlugin{installable.InternalName}", FontAwesomeIcon.Download)) { - this.installer.StartInstall(available, false); + this.installer.StartInstall(installable, false); } if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.InstallPlugin); } - + ImGui.SetCursorPos(before); } From a85c6315d4da311b5f9368eeb58358ac530e8f24 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:24:30 +0200 Subject: [PATCH 05/71] warnings --- Dalamud/Plugin/Internal/Profiles/Profile.cs | 6 +++--- Dalamud/Plugin/Internal/Profiles/ProfileManager.cs | 6 +++--- Dalamud/Plugin/Internal/Profiles/ProfileModel.cs | 4 ++-- Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs | 3 +++ Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs | 4 ++++ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index b9c90235a..d20b5c6bc 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -142,7 +142,7 @@ internal class Profile /// /// Check if this profile contains a specific plugin, and if it is enabled. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled. public bool? WantsPlugin(Guid workingPluginId) { @@ -157,7 +157,7 @@ internal class Profile /// Add a plugin to this profile with the desired state, or change the state of a plugin in this profile. /// This will block until all states have been applied. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the plugin should be enabled. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. @@ -198,7 +198,7 @@ internal class Profile /// Remove a plugin from this profile. /// This will block until all states have been applied. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. public async Task RemoveAsync(Guid workingPluginId, bool apply = true) diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index d8f091e9f..6b51f7535 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -69,7 +69,7 @@ internal class ProfileManager : IServiceType /// /// Check if any enabled profile wants a specific plugin enabled. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// The state the plugin shall be in, if it needs to be added. /// Whether or not the plugin should be added to the default preset, if it's not present in any preset. /// Whether or not the plugin shall be enabled. @@ -105,7 +105,7 @@ internal class ProfileManager : IServiceType /// /// Check whether a plugin is declared in any profile. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the plugin is in any profile. public bool IsInAnyProfile(Guid workingPluginId) { @@ -117,7 +117,7 @@ internal class ProfileManager : IServiceType /// Check whether a plugin is only in the default profile. /// A plugin can never be in the default profile if it is in any other profile. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the plugin is in the default profile. public bool IsInDefaultProfile(Guid workingPluginId) => this.DefaultProfile.WantsPlugin(workingPluginId) != null; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs index d77cab443..e3d9e2955 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Reflection; + using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs index 1b224c8dc..99da4263b 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs @@ -47,6 +47,9 @@ public class ProfileModelV1 : ProfileModel /// public string? InternalName { get; set; } + /// + /// Gets or sets an ID uniquely identifying this specific instance of a plugin. + /// public Guid WorkingPluginId { get; set; } /// diff --git a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs index 2c10def99..7909981bc 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs @@ -9,6 +9,7 @@ internal class ProfilePluginEntry /// Initializes a new instance of the class. /// /// The internal name of the plugin. + /// The ID of the plugin. /// A value indicating whether or not this entry is enabled. public ProfilePluginEntry(string internalName, Guid workingPluginId, bool state) { @@ -22,6 +23,9 @@ internal class ProfilePluginEntry /// public string InternalName { get; } + /// + /// Gets or sets an ID uniquely identifying this specific instance of a plugin. + /// public Guid WorkingPluginId { get; set; } /// From 8bdab4d2c8368edb37acdc4f0f7d17d75c40f753 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 21 Nov 2023 15:09:38 +0900 Subject: [PATCH 06/71] Implement DalamudFontAtlas --- Dalamud/Interface/GameFonts/FdtFileView.cs | 159 ++ .../GameFonts/GameFontFamilyAndSize.cs | 25 +- .../GameFontFamilyAndSizeAttribute.cs | 37 + Dalamud/Interface/GameFonts/GameFontHandle.cs | 83 +- .../Interface/GameFonts/GameFontManager.cs | 507 ------ Dalamud/Interface/GameFonts/GameFontStyle.cs | 2 +- .../Interface/Internal/DalamudInterface.cs | 13 +- .../Interface/Internal/InterfaceManager.cs | 932 +++-------- .../Internal/Windows/ChangelogWindow.cs | 62 +- .../Internal/Windows/Data/DataWindow.cs | 8 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 186 +++ .../Windows/Settings/SettingsWindow.cs | 19 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 30 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 63 +- .../FontAtlasAutoRebuildMode.cs | 22 + .../ManagedFontAtlas/FontAtlasBuildStep.cs | 38 + .../FontAtlasBuildStepDelegate.cs | 15 + .../FontAtlasBuildToolkitUtilities.cs | 111 ++ .../Interface/ManagedFontAtlas/IFontAtlas.cs | 84 + .../IFontAtlasBuildToolkit.cs | 67 + .../IFontAtlasBuildToolkitPostBuild.cs | 26 + .../IFontAtlasBuildToolkitPostPromotion.cs | 33 + .../IFontAtlasBuildToolkitPreBuild.cs | 164 ++ .../Interface/ManagedFontAtlas/IFontHandle.cs | 42 + .../Internals/DelegateFontHandle.cs | 331 ++++ .../FontAtlasFactory.BuildToolkit.cs | 647 ++++++++ .../FontAtlasFactory.Implementation.cs | 711 +++++++++ .../Internals/FontAtlasFactory.cs | 379 +++++ .../Internals/GamePrebakedFontHandle.cs | 692 ++++++++ .../Internals/IFontHandleManager.cs | 34 + .../Internals/IFontHandleSubstance.cs | 47 + .../Internals/TrueType.Common.cs | 203 +++ .../Internals/TrueType.Enums.cs | 84 + .../Internals/TrueType.Files.cs | 148 ++ .../Internals/TrueType.GposGsub.cs | 259 +++ .../Internals/TrueType.PointerSpan.cs | 443 ++++++ .../Internals/TrueType.Tables.cs | 1391 +++++++++++++++++ .../ManagedFontAtlas/Internals/TrueType.cs | 135 ++ .../ManagedFontAtlas/SafeFontConfig.cs | 291 ++++ Dalamud/Interface/UiBuilder.cs | 203 ++- Dalamud/Interface/Utility/ImGuiHelpers.cs | 253 ++- 41 files changed, 7551 insertions(+), 1428 deletions(-) create mode 100644 Dalamud/Interface/GameFonts/FdtFileView.cs create mode 100644 Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs delete mode 100644 Dalamud/Interface/GameFonts/GameFontManager.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs diff --git a/Dalamud/Interface/GameFonts/FdtFileView.cs b/Dalamud/Interface/GameFonts/FdtFileView.cs new file mode 100644 index 000000000..78b2e22f3 --- /dev/null +++ b/Dalamud/Interface/GameFonts/FdtFileView.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.IO; + +namespace Dalamud.Interface.GameFonts; + +/// +/// Reference member view of a .fdt file data. +/// +internal readonly unsafe ref struct FdtFileView +{ + private readonly byte* ptr; + + /// + /// Initializes a new instance of the struct. + /// + /// Pointer to the data. + /// Length of the data. + public FdtFileView(void* ptr, int length) + { + this.ptr = (byte*)ptr; + if (length < sizeof(FdtReader.FdtHeader)) + throw new InvalidDataException("Not enough space for a FdtHeader"); + + if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader)) + throw new InvalidDataException("Not enough space for a FontTableHeader"); + if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader) + + (sizeof(FdtReader.FontTableEntry) * this.FontHeader.FontTableEntryCount)) + throw new InvalidDataException("Not enough space for all the FontTableEntry"); + + if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader)) + throw new InvalidDataException("Not enough space for a KerningTableHeader"); + if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader) + + (sizeof(FdtReader.KerningTableEntry) * this.KerningEntryCount)) + throw new InvalidDataException("Not enough space for all the KerningTableEntry"); + } + + /// + /// Gets the file header. + /// + public ref FdtReader.FdtHeader FileHeader => ref *(FdtReader.FdtHeader*)this.ptr; + + /// + /// Gets the font header. + /// + public ref FdtReader.FontTableHeader FontHeader => + ref *(FdtReader.FontTableHeader*)((nint)this.ptr + this.FileHeader.FontTableHeaderOffset); + + /// + /// Gets the glyphs. + /// + public Span Glyphs => new(this.GlyphsUnsafe, this.FontHeader.FontTableEntryCount); + + /// + /// Gets the kerning header. + /// + public ref FdtReader.KerningTableHeader KerningHeader => + ref *(FdtReader.KerningTableHeader*)((nint)this.ptr + this.FileHeader.KerningTableHeaderOffset); + + /// + /// Gets the number of kerning entries. + /// + public int KerningEntryCount => Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); + + /// + /// Gets the kerning entries. + /// + public Span PairAdjustments => new( + this.ptr + this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader), + this.KerningEntryCount); + + /// + /// Gets the maximum texture index. + /// + public int MaxTextureIndex + { + get + { + var i = 0; + foreach (ref var g in this.Glyphs) + { + if (g.TextureIndex > i) + i = g.TextureIndex; + } + + return i; + } + } + + private FdtReader.FontTableEntry* GlyphsUnsafe => + (FdtReader.FontTableEntry*)(this.ptr + this.FileHeader.FontTableHeaderOffset + + sizeof(FdtReader.FontTableHeader)); + + /// + /// Finds the glyph index for the corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding index, or a negative number according to . + public int FindGlyphIndex(int codepoint) + { + var comp = FdtReader.CodePointToUtf8Int32(codepoint); + + var glyphs = this.GlyphsUnsafe; + var lo = 0; + var hi = this.FontHeader.FontTableEntryCount - 1; + while (lo <= hi) + { + var i = (int)(((uint)hi + (uint)lo) >> 1); + switch (comp.CompareTo(glyphs[i].CharUtf8)) + { + case 0: + return i; + case > 0: + lo = i + 1; + break; + default: + hi = i - 1; + break; + } + } + + return ~lo; + } + + /// + /// Create a glyph range for use with . + /// + /// Merge two ranges into one if distance is below the value specified in this parameter. + /// Glyph ranges. + public ushort[] ToGlyphRanges(int mergeDistance = 8) + { + var glyphs = this.Glyphs; + var ranges = new List(glyphs.Length) + { + checked((ushort)glyphs[0].CharInt), + checked((ushort)glyphs[0].CharInt), + }; + + foreach (ref var glyph in glyphs[1..]) + { + var c32 = glyph.CharInt; + if (c32 >= 0x10000) + break; + + var c16 = unchecked((ushort)c32); + if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) + { + ranges[^1] = c16; + } + else if (ranges[^1] + 1 < c16) + { + ranges.Add(c16); + ranges.Add(c16); + } + } + + ranges.Add(0); + return ranges.ToArray(); + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs index dd78baf87..6e66cf19b 100644 --- a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs @@ -3,7 +3,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Enum of available game fonts in specific sizes. /// -public enum GameFontFamilyAndSize : int +public enum GameFontFamilyAndSize { /// /// Placeholder meaning unused. @@ -15,6 +15,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_96.fdt", "common/font/font{0}.tex", -1)] Axis96, /// @@ -22,6 +23,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_12.fdt", "common/font/font{0}.tex", -1)] Axis12, /// @@ -29,6 +31,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_14.fdt", "common/font/font{0}.tex", -1)] Axis14, /// @@ -36,6 +39,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_18.fdt", "common/font/font{0}.tex", -1)] Axis18, /// @@ -43,6 +47,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_36.fdt", "common/font/font{0}.tex", -4)] Axis36, /// @@ -50,6 +55,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_16.fdt", "common/font/font{0}.tex", -1)] Jupiter16, /// @@ -57,6 +63,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_20.fdt", "common/font/font{0}.tex", -1)] Jupiter20, /// @@ -64,6 +71,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_23.fdt", "common/font/font{0}.tex", -1)] Jupiter23, /// @@ -71,6 +79,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// + [GameFontFamilyAndSize("common/font/Jupiter_45.fdt", "common/font/font{0}.tex", -2)] Jupiter45, /// @@ -78,6 +87,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_46.fdt", "common/font/font{0}.tex", -2)] Jupiter46, /// @@ -85,6 +95,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// + [GameFontFamilyAndSize("common/font/Jupiter_90.fdt", "common/font/font{0}.tex", -4)] Jupiter90, /// @@ -92,6 +103,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_16.fdt", "common/font/font{0}.tex", -1)] Meidinger16, /// @@ -99,6 +111,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_20.fdt", "common/font/font{0}.tex", -1)] Meidinger20, /// @@ -106,6 +119,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_40.fdt", "common/font/font{0}.tex", -4)] Meidinger40, /// @@ -113,6 +127,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_10.fdt", "common/font/font{0}.tex", -1)] MiedingerMid10, /// @@ -120,6 +135,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_12.fdt", "common/font/font{0}.tex", -1)] MiedingerMid12, /// @@ -127,6 +143,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_14.fdt", "common/font/font{0}.tex", -1)] MiedingerMid14, /// @@ -134,6 +151,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_18.fdt", "common/font/font{0}.tex", -1)] MiedingerMid18, /// @@ -141,6 +159,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_36.fdt", "common/font/font{0}.tex", -2)] MiedingerMid36, /// @@ -148,6 +167,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_184.fdt", "common/font/font{0}.tex", -1)] TrumpGothic184, /// @@ -155,6 +175,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_23.fdt", "common/font/font{0}.tex", -1)] TrumpGothic23, /// @@ -162,6 +183,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_34.fdt", "common/font/font{0}.tex", -1)] TrumpGothic34, /// @@ -169,5 +191,6 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_68.fdt", "common/font/font{0}.tex", -3)] TrumpGothic68, } diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs new file mode 100644 index 000000000..f5260e4bc --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs @@ -0,0 +1,37 @@ +namespace Dalamud.Interface.GameFonts; + +/// +/// Marks the path for an enum value. +/// +[AttributeUsage(AttributeTargets.Field)] +internal class GameFontFamilyAndSizeAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Inner path of the file. + /// the file path format for the relevant .tex files. + /// Horizontal offset of the corresponding font. + public GameFontFamilyAndSizeAttribute(string path, string texPathFormat, int horizontalOffset) + { + this.Path = path; + this.TexPathFormat = texPathFormat; + this.HorizontalOffset = horizontalOffset; + } + + /// + /// Gets the path. + /// + public string Path { get; } + + /// + /// Gets the file path format for the relevant .tex files.
+ /// Used for (, ). + ///
+ public string TexPathFormat { get; } + + /// + /// Gets the horizontal offset of the corresponding font. + /// + public int HorizontalOffset { get; } +} diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index d71e725c5..77461aa0a 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,75 +1,76 @@ -using System; using System.Numerics; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; + using ImGuiNET; namespace Dalamud.Interface.GameFonts; /// -/// Prepare and keep game font loaded for use in OnDraw. +/// ABI-compatible wrapper for . /// -public class GameFontHandle : IDisposable +public sealed class GameFontHandle : IFontHandle { - private readonly GameFontManager manager; - private readonly GameFontStyle fontStyle; + private readonly IFontHandle.IInternal fontHandle; + private readonly FontAtlasFactory fontAtlasFactory; /// /// Initializes a new instance of the class. /// - /// GameFontManager instance. - /// Font to use. - internal GameFontHandle(GameFontManager manager, GameFontStyle font) + /// The wrapped . + /// An instance of . + internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory) { - this.manager = manager; - this.fontStyle = font; + this.fontHandle = fontHandle; + this.fontAtlasFactory = fontAtlasFactory; } - /// - /// Gets the font style. - /// - public GameFontStyle Style => this.fontStyle; + /// + public Exception? LoadException => this.fontHandle.LoadException; + + /// + public bool Available => this.fontHandle.Available; + + /// + [Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)] + public ImFontPtr ImFont => this.fontHandle.ImFont; /// - /// Gets a value indicating whether this font is ready for use. + /// Gets the font style. Only applicable for . /// - public bool Available - { - get - { - unsafe - { - return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null; - } - } - } + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public GameFontStyle Style => ((GamePrebakedFontHandle)this.fontHandle).FontStyle; /// - /// Gets the font. + /// Gets the relevant .
+ ///
+ /// Only applicable for game fonts. Otherwise it will throw. ///
- public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value; + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public FdtReader FdtReader => this.fontAtlasFactory.GetFdtReader(this.Style.FamilyAndSize)!; + + /// + public void Dispose() => this.fontHandle.Dispose(); + + /// + public IDisposable Push() => this.fontHandle.Push(); /// - /// Gets the FdtReader. - /// - public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); - - /// - /// Creates a new GameFontLayoutPlan.Builder. + /// Creates a new .
+ ///
+ /// Only applicable for game fonts. Otherwise it will throw. ///
/// Text. /// A new builder for GameFontLayoutPlan. - public GameFontLayoutPlan.Builder LayoutBuilder(string text) - { - return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text); - } - - /// - public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public GameFontLayoutPlan.Builder LayoutBuilder(string text) => new(this.ImFont, this.FdtReader, text); /// /// Draws text. /// /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void Text(string text) { if (!this.Available) @@ -93,6 +94,7 @@ public class GameFontHandle : IDisposable ///
/// Color. /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextColored(Vector4 col, string text) { ImGui.PushStyleColor(ImGuiCol.Text, col); @@ -104,6 +106,7 @@ public class GameFontHandle : IDisposable /// Draws disabled text. ///
/// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextDisabled(string text) { unsafe diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs deleted file mode 100644 index b3454e085..000000000 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ /dev/null @@ -1,507 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Utility.Timing; -using ImGuiNET; -using Lumina.Data.Files; -using Serilog; - -using static Dalamud.Interface.Utility.ImGuiHelpers; - -namespace Dalamud.Interface.GameFonts; - -/// -/// Loads game font for use in ImGui. -/// -[ServiceManager.BlockingEarlyLoadedService] -internal class GameFontManager : IServiceType -{ - private static readonly string?[] FontNames = - { - null, - "AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36", - "Jupiter_16", "Jupiter_20", "Jupiter_23", "Jupiter_45", "Jupiter_46", "Jupiter_90", - "Meidinger_16", "Meidinger_20", "Meidinger_40", - "MiedingerMid_10", "MiedingerMid_12", "MiedingerMid_14", "MiedingerMid_18", "MiedingerMid_36", - "TrumpGothic_184", "TrumpGothic_23", "TrumpGothic_34", "TrumpGothic_68", - }; - - private readonly object syncRoot = new(); - - private readonly FdtReader?[] fdts; - private readonly List texturePixels; - private readonly Dictionary fonts = new(); - private readonly Dictionary fontUseCounter = new(); - private readonly Dictionary>> glyphRectIds = new(); - -#pragma warning disable CS0414 - private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; -#pragma warning restore CS0414 - - [ServiceManager.ServiceConstructor] - private GameFontManager(DataManager dataManager) - { - using (Timings.Start("Getting fdt data")) - { - this.fdts = FontNames.Select(fontName => fontName == null ? null : new FdtReader(dataManager.GetFile($"common/font/{fontName}.fdt")!.Data)).ToArray(); - } - - using (Timings.Start("Getting texture data")) - { - var texTasks = Enumerable - .Range(1, 1 + this.fdts - .Where(x => x != null) - .Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max()) - .Max()) - .Select(x => dataManager.GetFile($"common/font/font{x}.tex")!) - .Select(x => new Task(Timings.AttachTimingHandle(() => x.ImageData!))) - .ToArray(); - foreach (var task in texTasks) - task.Start(); - this.texturePixels = texTasks.Select(x => x.GetAwaiter().GetResult()).ToList(); - } - } - - /// - /// Describe font into a string. - /// - /// Font to describe. - /// A string in a form of "FontName (NNNpt)". - public static string DescribeFont(GameFontFamilyAndSize font) - { - return font switch - { - GameFontFamilyAndSize.Undefined => "-", - GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)", - GameFontFamilyAndSize.Axis12 => "AXIS (12pt)", - GameFontFamilyAndSize.Axis14 => "AXIS (14pt)", - GameFontFamilyAndSize.Axis18 => "AXIS (18pt)", - GameFontFamilyAndSize.Axis36 => "AXIS (36pt)", - GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)", - GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)", - GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)", - GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)", - GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)", - GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)", - GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)", - GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)", - GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)", - GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)", - GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)", - GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)", - GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)", - GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)", - GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)", - GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)", - GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)", - GameFontFamilyAndSize.TrumpGothic68 => "Trump Gothic (68pt)", - _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), - }; - } - - /// - /// Determines whether a font should be able to display most of stuff. - /// - /// Font to check. - /// True if it can. - public static bool IsGenericPurposeFont(GameFontFamilyAndSize font) - { - return font switch - { - GameFontFamilyAndSize.Axis96 => true, - GameFontFamilyAndSize.Axis12 => true, - GameFontFamilyAndSize.Axis14 => true, - GameFontFamilyAndSize.Axis18 => true, - GameFontFamilyAndSize.Axis36 => true, - _ => false, - }; - } - - /// - /// Unscales fonts after they have been rendered onto atlas. - /// - /// Font to unscale. - /// Scale factor. - /// Whether to call target.BuildLookupTable(). - public static void UnscaleFont(ImFontPtr fontPtr, float fontScale, bool rebuildLookupTable = true) - { - if (fontScale == 1) - return; - - unsafe - { - var font = fontPtr.NativePtr; - for (int i = 0, i_ = font->IndexedHotData.Size; i < i_; ++i) - { - font->IndexedHotData.Ref(i).AdvanceX /= fontScale; - font->IndexedHotData.Ref(i).OccupiedWidth /= fontScale; - } - - font->FontSize /= fontScale; - font->Ascent /= fontScale; - font->Descent /= fontScale; - if (font->ConfigData != null) - font->ConfigData->SizePixels /= fontScale; - var glyphs = (ImFontGlyphReal*)font->Glyphs.Data; - for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++) - { - var glyph = &glyphs[i]; - glyph->X0 /= fontScale; - glyph->X1 /= fontScale; - glyph->Y0 /= fontScale; - glyph->Y1 /= fontScale; - glyph->AdvanceX /= fontScale; - } - - for (int i = 0, i_ = font->KerningPairs.Size; i < i_; i++) - font->KerningPairs.Ref(i).AdvanceXAdjustment /= fontScale; - for (int i = 0, i_ = font->FrequentKerningPairs.Size; i < i_; i++) - font->FrequentKerningPairs.Ref(i) /= fontScale; - } - - if (rebuildLookupTable && fontPtr.Glyphs.Size > 0) - fontPtr.BuildLookupTableNonstandard(); - } - - /// - /// Create a glyph range for use with ImGui AddFont. - /// - /// Font family and size. - /// Merge two ranges into one if distance is below the value specified in this parameter. - /// Glyph ranges. - public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8) - { - var fdt = this.fdts[(int)family]!; - var ranges = new List(fdt.Glyphs.Count) - { - checked((ushort)fdt.Glyphs[0].CharInt), - checked((ushort)fdt.Glyphs[0].CharInt), - }; - - foreach (var glyph in fdt.Glyphs.Skip(1)) - { - var c32 = glyph.CharInt; - if (c32 >= 0x10000) - break; - - var c16 = unchecked((ushort)c32); - if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) - { - ranges[^1] = c16; - } - else if (ranges[^1] + 1 < c16) - { - ranges.Add(c16); - ranges.Add(c16); - } - } - - return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned); - } - - /// - /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. - /// - /// Font to use. - /// Handle to game font that may or may not be ready yet. - public GameFontHandle NewFontRef(GameFontStyle style) - { - var interfaceManager = Service.Get(); - var needRebuild = false; - - lock (this.syncRoot) - { - this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1; - } - - needRebuild = !this.fonts.ContainsKey(style); - if (needRebuild) - { - Log.Information("[GameFontManager] NewFontRef: Queueing RebuildFonts because {0} has been requested.", style.ToString()); - Service.GetAsync() - .ContinueWith(task => task.Result.RunOnTick(() => interfaceManager.RebuildFonts())); - } - - return new(this, style); - } - - /// - /// Gets the font. - /// - /// Font to get. - /// Corresponding font or null. - public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null); - - /// - /// Gets the corresponding FdtReader. - /// - /// Font to get. - /// Corresponding FdtReader or null. - public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family]; - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable); - } - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable); - } - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); - } - - /// - /// Build fonts before plugins do something more. To be called from InterfaceManager. - /// - public void BuildFonts() - { - this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = true; - - this.glyphRectIds.Clear(); - this.fonts.Clear(); - - lock (this.syncRoot) - { - foreach (var style in this.fontUseCounter.Keys) - this.EnsureFont(style); - } - } - - /// - /// Record that ImGui.GetIO().Fonts.Build() has been called. - /// - public void AfterIoFontsBuild() - { - this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; - } - - /// - /// Checks whether GameFontMamager owns an ImFont. - /// - /// ImFontPtr to check. - /// Whether it owns. - public bool OwnsFont(ImFontPtr fontPtr) => this.fonts.ContainsValue(fontPtr); - - /// - /// Post-build fonts before plugins do something more. To be called from InterfaceManager. - /// - public unsafe void AfterBuildFonts() - { - var interfaceManager = Service.Get(); - var ioFonts = ImGui.GetIO().Fonts; - var fontGamma = interfaceManager.FontGamma; - - var pixels8s = new byte*[ioFonts.Textures.Size]; - var pixels32s = new uint*[ioFonts.Textures.Size]; - var widths = new int[ioFonts.Textures.Size]; - var heights = new int[ioFonts.Textures.Size]; - for (var i = 0; i < pixels8s.Length; i++) - { - ioFonts.GetTexDataAsRGBA32(i, out pixels8s[i], out widths[i], out heights[i]); - pixels32s[i] = (uint*)pixels8s[i]; - } - - foreach (var (style, font) in this.fonts) - { - var fdt = this.fdts[(int)style.FamilyAndSize]; - var scale = style.SizePt / fdt.FontHeader.Size; - var fontPtr = font.NativePtr; - - Log.Verbose("[GameFontManager] AfterBuildFonts: Scaling {0} from {1}pt to {2}pt (scale: {3})", style.ToString(), fdt.FontHeader.Size, style.SizePt, scale); - - fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; - if (fontPtr->ConfigData != null) - fontPtr->ConfigData->SizePixels = fontPtr->FontSize; - fontPtr->Ascent = fdt.FontHeader.Ascent; - fontPtr->Descent = fdt.FontHeader.Descent; - fontPtr->EllipsisChar = '…'; - foreach (var fallbackCharCandidate in "〓?!") - { - var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); - if ((IntPtr)glyph.NativePtr != IntPtr.Zero) - { - var ptr = font.NativePtr; - ptr->FallbackChar = fallbackCharCandidate; - ptr->FallbackGlyph = glyph.NativePtr; - ptr->FallbackHotData = (ImFontGlyphHotData*)ptr->IndexedHotData.Address(fallbackCharCandidate); - break; - } - } - - // I have no idea what's causing NPE, so just to be safe - try - { - if (font.NativePtr != null && font.NativePtr->ConfigData != null) - { - var nameBytes = Encoding.UTF8.GetBytes(style.ToString() + "\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); - } - } - catch (NullReferenceException) - { - // do nothing - } - - foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style]) - { - var rc = (ImFontAtlasCustomRectReal*)ioFonts.GetCustomRectByIndex(rectId).NativePtr; - var pixels8 = pixels8s[rc->TextureIndex]; - var pixels32 = pixels32s[rc->TextureIndex]; - var width = widths[rc->TextureIndex]; - var height = heights[rc->TextureIndex]; - var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; - var sourceBufferDelta = glyph.TextureChannelByteIndex; - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - if (widthAdjustment == 0) - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; - pixels32[((rc->Y + y) * width) + rc->X + x] = (uint)(a << 24) | 0xFFFFFFu; - } - } - } - else - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) - pixels32[((rc->Y + y) * width) + rc->X + x] = 0xFFFFFFu; - } - - for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++) - { - var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); - for (var y = 0; y < glyph.BoundingHeight; y++) - { - float xDelta = xbold; - if (style.BaseSkewStrength > 0) - xDelta += style.BaseSkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; - else if (style.BaseSkewStrength < 0) - xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; - var xDeltaInt = (int)Math.Floor(xDelta); - var xness = xDelta - xDeltaInt; - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x; - var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; - var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))]; - var n = (a1 * xness) + (a2 * (1 - xness)); - var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; - pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n)); - } - } - } - } - - if (Math.Abs(fontGamma - 1.4f) >= 0.001) - { - // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) - for (int y = rc->Y, y_ = rc->Y + rc->Height; y < y_; y++) - { - for (int x = rc->X, x_ = rc->X + rc->Width; x < x_; x++) - { - var i = (((y * width) + x) * 4) + 3; - pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); - } - } - } - } - - UnscaleFont(font, 1 / scale, false); - } - } - - /// - /// Decrease font reference counter. - /// - /// Font to release. - internal void DecreaseFontRef(GameFontStyle style) - { - lock (this.syncRoot) - { - if (!this.fontUseCounter.ContainsKey(style)) - return; - - if ((this.fontUseCounter[style] -= 1) == 0) - this.fontUseCounter.Remove(style); - } - } - - private unsafe void EnsureFont(GameFontStyle style) - { - var rectIds = this.glyphRectIds[style] = new(); - - var fdt = this.fdts[(int)style.FamilyAndSize]; - if (fdt == null) - return; - - ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - fontConfig.PixelSnapH = false; - - var io = ImGui.GetIO(); - var font = io.Fonts.AddFontDefault(fontConfig); - - fontConfig.Destroy(); - - this.fonts[style] = font; - foreach (var glyph in fdt.Glyphs) - { - var c = glyph.Char; - if (c < 32 || c >= 0xFFFF) - continue; - - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - rectIds[c] = Tuple.Create( - io.Fonts.AddCustomRectFontGlyph( - font, - c, - glyph.BoundingWidth + widthAdjustment, - glyph.BoundingHeight, - glyph.AdvanceWidth, - new Vector2(0, glyph.CurrentOffsetY)), - glyph); - } - - foreach (var kernPair in fdt.Distances) - font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); - } -} diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index 946473df4..e219670b8 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -175,7 +175,7 @@ public struct GameFontStyle public bool Italic { get => this.SkewStrength != 0; - set => this.SkewStrength = value ? this.SizePx / 7 : 0; + set => this.SkewStrength = value ? this.SizePx / 6 : 0; } /// diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 95415659b..60c1f9957 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -21,6 +21,7 @@ using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -93,7 +94,8 @@ internal class DalamudInterface : IDisposable, IServiceType private DalamudInterface( Dalamud dalamud, DalamudConfiguration configuration, - InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, + FontAtlasFactory fontAtlasFactory, + InterfaceManager interfaceManager, PluginImageCache pluginImageCache, DalamudAssetManager dalamudAssetManager, Game.Framework framework, @@ -103,7 +105,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.dalamud = dalamud; this.configuration = configuration; - this.interfaceManager = interfaceManagerWithScene.Manager; + this.interfaceManager = interfaceManager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -122,10 +124,14 @@ internal class DalamudInterface : IDisposable, IServiceType clientState, configuration, dalamudAssetManager, + fontAtlasFactory, framework, gameGui, titleScreenMenu) { IsOpen = false }; - this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; + this.changelogWindow = new ChangelogWindow( + this.titleScreenMenuWindow, + fontAtlasFactory, + dalamudAssetManager) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false }; @@ -207,6 +213,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.interfaceManager.Draw -= this.OnDraw; + this.WindowSystem.Windows.OfType().AggregateToDisposable().Dispose(); this.WindowSystem.RemoveAllWindows(); this.changelogWindow.Dispose(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 48157fa86..5a6a2cbdb 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -19,10 +18,13 @@ using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Storage.Assets; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -64,11 +66,9 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. - private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. - - private readonly HashSet glyphRequests = new(); - private readonly Dictionary loadedFontInfo = new(); + private const int NonMainThreadFontAccessWarningCheckInterval = 10000; + private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); + private static long nextNonMainThreadFontAccessWarningCheck; private readonly List deferredDisposeTextures = new(); @@ -81,28 +81,28 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); - private readonly ManualResetEvent fontBuildSignal; - private readonly SwapChainVtableResolver address; + private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; private RawDX11Scene? scene; private Hook? presentHook; private Hook? resizeBuffersHook; + private IFontAtlas? dalamudAtlas; + private IFontHandle.IInternal? defaultFontHandle; + private IFontHandle.IInternal? iconFontHandle; + private IFontHandle.IInternal? monoFontHandle; + // can't access imgui IO before first present call private bool lastWantCapture = false; - private bool isRebuildingFonts = false; private bool isOverrideGameCursor = true; + private IntPtr gameWindowHandle; [ServiceManager.ServiceConstructor] private InterfaceManager() { this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); - - this.fontBuildSignal = new ManualResetEvent(false); - - this.address = new SwapChainVtableResolver(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -117,43 +117,46 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// This event gets called each frame to facilitate ImGui drawing. /// - public event RawDX11Scene.BuildUIDelegate Draw; + public event RawDX11Scene.BuildUIDelegate? Draw; /// /// This event gets called when ResizeBuffers is called. /// - public event Action ResizeBuffers; - - /// - /// Gets or sets an action that is executed right before fonts are rebuilt. - /// - public event Action BuildFonts; + public event Action? ResizeBuffers; /// /// Gets or sets an action that is executed right after fonts are rebuilt. /// - public event Action AfterBuildFonts; + public event Action? AfterBuildFonts; /// - /// Gets the default ImGui font. + /// Gets the default ImGui font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr DefaultFont { get; private set; } + public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// - /// Gets an included FontAwesome icon font. + /// Gets an included FontAwesome icon font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr IconFont { get; private set; } + public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// - /// Gets an included monospaced font. + /// Gets an included monospaced font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr MonoFont { get; private set; } + public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. /// public ImGuiIOPtr LastImGuiIoPtr { get; set; } + /// + /// Gets the DX11 scene. + /// + public RawDX11Scene? Scene => this.scene; + /// /// Gets the D3D11 device instance. /// @@ -178,11 +181,6 @@ internal class InterfaceManager : IDisposable, IServiceType } } - /// - /// Gets or sets a value indicating whether the fonts are built and ready to use. - /// - public bool FontsReady { get; set; } = false; - /// /// Gets a value indicating whether the Dalamud interface ready to use. /// @@ -213,30 +211,52 @@ internal class InterfaceManager : IDisposable, IServiceType /// public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGammaLevel)); - /// - /// Gets a value indicating whether we're building fonts but haven't generated atlas yet. - /// - public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0); - /// /// Gets a value indicating the native handle of the game main window. /// - public IntPtr GameWindowHandle { get; private set; } + public IntPtr GameWindowHandle + { + get + { + if (this.gameWindowHandle == 0) + { + nint gwh = 0; + while ((gwh = NativeFunctions.FindWindowEx(0, gwh, "FFXIVGAME", 0)) != 0) + { + _ = User32.GetWindowThreadProcessId(gwh, out var pid); + if (pid == Environment.ProcessId && User32.IsWindowVisible(gwh)) + { + this.gameWindowHandle = gwh; + break; + } + } + } + + return this.gameWindowHandle; + } + } /// /// Dispose of managed and unmanaged resources. /// public void Dispose() { - this.framework.RunOnFrameworkThread(() => + if (Service.GetNullable() is { } framework) + framework.RunOnFrameworkThread(Disposer).Wait(); + else + Disposer(); + + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + this.dalamudAtlas?.Dispose(); + this.scene?.Dispose(); + return; + + void Disposer() { this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - }).Wait(); - - this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; - this.scene?.Dispose(); + } } #nullable enable @@ -376,93 +396,8 @@ internal class InterfaceManager : IDisposable, IServiceType /// public void RebuildFonts() { - if (this.scene == null) - { - Log.Verbose("[FONT] RebuildFonts(): scene not ready, doing nothing"); - return; - } - Log.Verbose("[FONT] RebuildFonts() called"); - - // don't invoke this multiple times per frame, in case multiple plugins call it - if (!this.isRebuildingFonts) - { - Log.Verbose("[FONT] RebuildFonts() trigger"); - this.isRebuildingFonts = true; - this.scene.OnNewRenderFrame += this.RebuildFontsInternal; - } - } - - /// - /// Wait for the rebuilding fonts to complete. - /// - public void WaitForFontRebuild() - { - this.fontBuildSignal.WaitOne(); - } - - /// - /// Requests a default font of specified size to exist. - /// - /// Font size in pixels. - /// Ranges of glyphs. - /// Requets handle. - public SpecialGlyphRequest NewFontSizeRef(float size, List> ranges) - { - var allContained = false; - var fonts = ImGui.GetIO().Fonts.Fonts; - ImFontPtr foundFont = null; - unsafe - { - for (int i = 0, i_ = fonts.Size; i < i_; i++) - { - if (!this.glyphRequests.Any(x => x.FontInternal.NativePtr == fonts[i].NativePtr)) - continue; - - allContained = true; - foreach (var range in ranges) - { - if (!allContained) - break; - - for (var j = range.Item1; j <= range.Item2 && allContained; j++) - allContained &= fonts[i].FindGlyphNoFallback(j).NativePtr != null; - } - - if (allContained) - foundFont = fonts[i]; - - break; - } - } - - var req = new SpecialGlyphRequest(this, size, ranges); - req.FontInternal = foundFont; - - if (!allContained) - this.RebuildFonts(); - - return req; - } - - /// - /// Requests a default font of specified size to exist. - /// - /// Font size in pixels. - /// Text to calculate glyph ranges from. - /// Requets handle. - public SpecialGlyphRequest NewFontSizeRef(float size, string text) - { - List> ranges = new(); - foreach (var c in new SortedSet(text.ToHashSet())) - { - if (ranges.Any() && ranges[^1].Item2 + 1 == c) - ranges[^1] = Tuple.Create(ranges[^1].Item1, c); - else - ranges.Add(Tuple.Create(c, c)); - } - - return this.NewFontSizeRef(size, ranges); + this.dalamudAtlas?.BuildFontsAsync(); } /// @@ -486,11 +421,11 @@ internal class InterfaceManager : IDisposable, IServiceType try { var dxgiDev = this.Device.QueryInterfaceOrNull(); - var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); + var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); if (dxgiAdapter == null) return null; - var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, SharpDX.DXGI.MemorySegmentGroup.Local); + var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, MemorySegmentGroup.Local); return (memInfo.CurrentUsage, memInfo.CurrentReservation); } catch @@ -516,20 +451,65 @@ internal class InterfaceManager : IDisposable, IServiceType /// Value. internal void SetImmersiveMode(bool enabled) { - if (this.GameWindowHandle == nint.Zero) - return; - - int value = enabled ? 1 : 0; - var hr = NativeFunctions.DwmSetWindowAttribute( - this.GameWindowHandle, - NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - ref value, - sizeof(int)); + if (this.GameWindowHandle == 0) + throw new InvalidOperationException("Game window is not yet ready."); + var value = enabled ? 1 : 0; + ((Result)NativeFunctions.DwmSetWindowAttribute( + this.GameWindowHandle, + NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + ref value, + sizeof(int))).CheckError(); } - private static void ShowFontError(string path) + private static InterfaceManager WhenFontsReady() { - Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error"); + var im = Service.GetNullable(); + if (im?.dalamudAtlas is not { } atlas) + throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call."); + + if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) + { + nextNonMainThreadFontAccessWarningCheck = + Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; + var stack = new StackTrace(); + if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) + { + if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) + { + NonMainThreadFontAccessWarning.Add(plugin, new()); + Log.Warning( + "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", + plugin.Name, + stack); + } + } + else + { + // Dalamud internal should be made safe right now + throw new InvalidOperationException("Attempted to access fonts outside the main thread."); + } + } + + if (!atlas.HasBuiltAtlas) + atlas.BuildTask.GetAwaiter().GetResult(); + return im; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RenderImGui(RawDX11Scene scene) + { + var conf = Service.Get(); + + // Process information needed by ImGuiHelpers each frame. + ImGuiHelpers.NewFrame(); + + // Enable viewports if there are no issues. + if (conf.IsDisableViewport || scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; + else + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + + scene.Render(); } private void InitScene(IntPtr swapChain) @@ -546,7 +526,7 @@ internal class InterfaceManager : IDisposable, IServiceType Service.ProvideException(ex); Log.Error(ex, "Could not load ImGui dependencies."); - var res = PInvoke.User32.MessageBox( + var res = User32.MessageBox( IntPtr.Zero, "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?", "Dalamud Error", @@ -578,7 +558,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (iniFileInfo.Length > 1200000) { Log.Warning("dalamudUI.ini was over 1mb, deleting"); - iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); + iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName!, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); iniFileInfo.Delete(); } } @@ -623,8 +603,6 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - this.SetupFonts(); - if (!configuration.IsDocking) { ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable; @@ -675,26 +653,34 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { + Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); + Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); + if (this.scene != null && swapChain != this.scene.SwapChain.NativePointer) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); if (this.scene == null) this.InitScene(swapChain); + Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); + + if (!this.dalamudAtlas!.HasBuiltAtlas) + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + if (this.address.IsReshade) { - var pRes = this.presentHook.Original(swapChain, syncInterval, presentFlags); + var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); - this.RenderImGui(); + RenderImGui(this.scene!); this.DisposeTextures(); return pRes; } - this.RenderImGui(); + RenderImGui(this.scene!); this.DisposeTextures(); - return this.presentHook.Original(swapChain, syncInterval, presentFlags); + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } private void DisposeTextures() @@ -711,471 +697,70 @@ internal class InterfaceManager : IDisposable, IServiceType } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RenderImGui() + [ServiceManager.CallWhenServicesReady( + "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] + private void ContinueConstruction( + TargetSigScanner sigScanner, + Framework framework, + FontAtlasFactory fontAtlasFactory) { - // Process information needed by ImGuiHelpers each frame. - ImGuiHelpers.NewFrame(); + this.dalamudAtlas = fontAtlasFactory + .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); + this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); + this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddFontAwesomeIconFont( + new() + { + SizePx = DefaultFontSizePx, + GlyphMinAdvanceX = DefaultFontSizePx, + GlyphMaxAdvanceX = DefaultFontSizePx, + }))); + this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddDalamudAssetFont( + DalamudAsset.InconsolataRegular, + new() { SizePx = DefaultFontSizePx }))); + this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( + tk => + { + // Note: the first call of this function is done outside the main thread; this is expected. + // Do not use DefaultFont, IconFont, and MonoFont. + // Use font handles directly. - // Check if we can still enable viewports without any issues. - this.CheckViewportState(); + // Fill missing glyphs in MonoFont from DefaultFont + tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); - this.scene.Render(); - } + // Broadcast to auto-rebuilding instances + this.AfterBuildFonts?.Invoke(); + }); - private void CheckViewportState() - { - var configuration = Service.Get(); + // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. + _ = this.dalamudAtlas.BuildFontsAsync(false); - if (configuration.IsDisableViewport || this.scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) - { - ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; - return; - } - - ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; - } - - /// - /// Loads font for use in ImGui text functions. - /// - private unsafe void SetupFonts() - { - using var setupFontsTimings = Timings.Start("IM SetupFonts"); - - var gameFontManager = Service.Get(); - var dalamud = Service.Get(); - var io = ImGui.GetIO(); - var ioFonts = io.Fonts; - - var fontGamma = this.FontGamma; - - this.fontBuildSignal.Reset(); - ioFonts.Clear(); - ioFonts.TexDesiredWidth = 4096; - - Log.Verbose("[FONT] SetupFonts - 1"); - - foreach (var v in this.loadedFontInfo) - v.Value.Dispose(); - - this.loadedFontInfo.Clear(); - - Log.Verbose("[FONT] SetupFonts - 2"); - - ImFontConfigPtr fontConfig = null; - List garbageList = new(); + this.address.Setup(sigScanner); try { - var dummyRangeHandle = GCHandle.Alloc(new ushort[] { '0', '0', 0 }, GCHandleType.Pinned); - garbageList.Add(dummyRangeHandle); - - fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - - var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Regular.otf"); - if (!File.Exists(fontPathJp)) - fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); - if (!File.Exists(fontPathJp)) - ShowFontError(fontPathJp); - Log.Verbose("[FONT] fontPathJp = {0}", fontPathJp); - - var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); - if (!File.Exists(fontPathKr)) - fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); - if (!File.Exists(fontPathKr)) - fontPathKr = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "malgun.ttf"); - if (!File.Exists(fontPathKr)) - fontPathKr = null; - Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); - - var fontPathChs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msyh.ttc"); - if (!File.Exists(fontPathChs)) - fontPathChs = null; - Log.Verbose("[FONT] fontPathChs = {0}", fontPathChs); - - var fontPathCht = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msjh.ttc"); - if (!File.Exists(fontPathCht)) - fontPathCht = null; - Log.Verbose("[FONT] fontPathChs = {0}", fontPathCht); - - // Default font - Log.Verbose("[FONT] SetupFonts - Default font"); - var fontInfo = new TargetFontModification( - "Default", - this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, - this.UseAxis ? DefaultFontSizePx : DefaultFontSizePx + 1, - io.FontGlobalScale); - Log.Verbose("[FONT] SetupFonts - Default corresponding AXIS size: {0}pt ({1}px)", fontInfo.SourceAxis.Style.BaseSizePt, fontInfo.SourceAxis.Style.BaseSizePx); - fontConfig.SizePixels = fontInfo.TargetSizePx * io.FontGlobalScale; - if (this.UseAxis) - { - fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = false; - DefaultFont = ioFonts.AddFontDefault(fontConfig); - this.loadedFontInfo[DefaultFont] = fontInfo; - } - else - { - var rangeHandle = gameFontManager.ToGlyphRanges(GameFontFamilyAndSize.Axis12); - garbageList.Add(rangeHandle); - - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontConfig.SizePixels, fontConfig); - this.loadedFontInfo[DefaultFont] = fontInfo; - } - - if (fontPathKr != null - && (Service.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul)) - { - fontConfig.MergeMode = true; - fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathKr, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - - if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") - { - fontConfig.MergeMode = true; - var rangeHandle = GCHandle.Alloc(new ushort[] - { - (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), - (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), - 0, - }, GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathCht, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" - || this.dalamudIme.EncounteredHan)) - { - fontConfig.MergeMode = true; - var rangeHandle = GCHandle.Alloc(new ushort[] - { - (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), - (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), - 0, - }, GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathChs, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - - // FontAwesome icon font - Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); - { - var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesomeFreeSolid.otf"); - if (!File.Exists(fontPathIcon)) - ShowFontError(fontPathIcon); - - var iconRangeHandle = GCHandle.Alloc(new ushort[] { 0xE000, 0xF8FF, 0, }, GCHandleType.Pinned); - garbageList.Add(iconRangeHandle); - - fontConfig.GlyphRanges = iconRangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, DefaultFontSizePx * io.FontGlobalScale, fontConfig); - this.loadedFontInfo[IconFont] = new("Icon", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); - } - - // Monospace font - Log.Verbose("[FONT] SetupFonts - Monospace font"); - { - var fontPathMono = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf"); - if (!File.Exists(fontPathMono)) - ShowFontError(fontPathMono); - - fontConfig.GlyphRanges = IntPtr.Zero; - fontConfig.PixelSnapH = true; - MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, DefaultFontSizePx * io.FontGlobalScale, fontConfig); - this.loadedFontInfo[MonoFont] = new("Mono", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); - } - - // Default font but in requested size for requested glyphs - Log.Verbose("[FONT] SetupFonts - Default font but in requested size for requested glyphs"); - { - Dictionary> extraFontRequests = new(); - foreach (var extraFontRequest in this.glyphRequests) - { - if (!extraFontRequests.ContainsKey(extraFontRequest.Size)) - extraFontRequests[extraFontRequest.Size] = new(); - extraFontRequests[extraFontRequest.Size].Add(extraFontRequest); - } - - foreach (var (fontSize, requests) in extraFontRequests) - { - List<(ushort, ushort)> codepointRanges = new(4 + requests.Sum(x => x.CodepointRanges.Count)) - { - new(Fallback1Codepoint, Fallback1Codepoint), - new(Fallback2Codepoint, Fallback2Codepoint), - // ImGui default ellipsis characters - new(0x2026, 0x2026), - new(0x0085, 0x0085), - }; - - foreach (var request in requests) - codepointRanges.AddRange(request.CodepointRanges.Select(x => (From: x.Item1, To: x.Item2))); - - codepointRanges.Sort(); - List flattenedRanges = new(); - foreach (var range in codepointRanges) - { - if (flattenedRanges.Any() && flattenedRanges[^1] >= range.Item1 - 1) - { - flattenedRanges[^1] = Math.Max(flattenedRanges[^1], range.Item2); - } - else - { - flattenedRanges.Add(range.Item1); - flattenedRanges.Add(range.Item2); - } - } - - flattenedRanges.Add(0); - - fontInfo = new( - $"Requested({fontSize}px)", - this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, - fontSize, - io.FontGlobalScale); - if (this.UseAxis) - { - fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.SizePixels = fontInfo.SourceAxis.Style.BaseSizePx; - fontConfig.PixelSnapH = false; - - var sizedFont = ioFonts.AddFontDefault(fontConfig); - this.loadedFontInfo[sizedFont] = fontInfo; - foreach (var request in requests) - request.FontInternal = sizedFont; - } - else - { - var rangeHandle = GCHandle.Alloc(flattenedRanges.ToArray(), GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.PixelSnapH = true; - - var sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontSize * io.FontGlobalScale, fontConfig, rangeHandle.AddrOfPinnedObject()); - this.loadedFontInfo[sizedFont] = fontInfo; - foreach (var request in requests) - request.FontInternal = sizedFont; - } - } - } - - gameFontManager.BuildFonts(); - - var customFontFirstConfigIndex = ioFonts.ConfigData.Size; - - Log.Verbose("[FONT] Invoke OnBuildFonts"); - this.BuildFonts?.InvokeSafely(); - Log.Verbose("[FONT] OnBuildFonts OK!"); - - for (int i = customFontFirstConfigIndex, i_ = ioFonts.ConfigData.Size; i < i_; i++) - { - var config = ioFonts.ConfigData[i]; - if (gameFontManager.OwnsFont(config.DstFont)) - continue; - - config.OversampleH = 1; - config.OversampleV = 1; - - var name = Encoding.UTF8.GetString((byte*)config.Name.Data, config.Name.Count).TrimEnd('\0'); - if (name.IsNullOrEmpty()) - name = $"{config.SizePixels}px"; - - // ImFont information is reflected only if corresponding ImFontConfig has MergeMode not set. - if (config.MergeMode) - { - if (!this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) - { - Log.Warning("MergeMode specified for {0} but not found in loadedFontInfo. Skipping.", name); - continue; - } - } - else - { - if (this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) - { - Log.Warning("MergeMode not specified for {0} but found in loadedFontInfo. Skipping.", name); - continue; - } - - // While the font will be loaded in the scaled size after FontScale is applied, the font will be treated as having the requested size when used from plugins. - this.loadedFontInfo[config.DstFont.NativePtr] = new($"PlReq({name})", config.SizePixels); - } - - config.SizePixels = config.SizePixels * io.FontGlobalScale; - } - - for (int i = 0, i_ = ioFonts.ConfigData.Size; i < i_; i++) - { - var config = ioFonts.ConfigData[i]; - config.RasterizerGamma *= fontGamma; - } - - Log.Verbose("[FONT] ImGui.IO.Build will be called."); - ioFonts.Build(); - gameFontManager.AfterIoFontsBuild(); - this.ClearStacks(); - Log.Verbose("[FONT] ImGui.IO.Build OK!"); - - gameFontManager.AfterBuildFonts(); - - foreach (var (font, mod) in this.loadedFontInfo) - { - // I have no idea what's causing NPE, so just to be safe - try - { - if (font.NativePtr != null && font.NativePtr->ConfigData != null) - { - var nameBytes = Encoding.UTF8.GetBytes($"{mod.Name}\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); - } - } - catch (NullReferenceException) - { - // do nothing - } - - Log.Verbose("[FONT] {0}: Unscale with scale value of {1}", mod.Name, mod.Scale); - GameFontManager.UnscaleFont(font, mod.Scale, false); - - if (mod.Axis == TargetFontModification.AxisMode.Overwrite) - { - Log.Verbose("[FONT] {0}: Overwrite from AXIS of size {1}px (was {2}px)", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); - GameFontManager.UnscaleFont(font, font.FontSize / mod.SourceAxis.ImFont.FontSize, false); - var ascentDiff = mod.SourceAxis.ImFont.Ascent - font.Ascent; - font.Ascent += ascentDiff; - font.Descent = ascentDiff; - font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar; - font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar; - ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); - } - else if (mod.Axis == TargetFontModification.AxisMode.GameGlyphsOnly) - { - Log.Verbose("[FONT] {0}: Overwrite game specific glyphs from AXIS of size {1}px", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); - if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) - mod.SourceAxis.ImFont.FontSize -= 1; - ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); - if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) - mod.SourceAxis.ImFont.FontSize += 1; - } - - Log.Verbose("[FONT] {0}: Resize from {1}px to {2}px", mod.Name, font.FontSize, mod.TargetSizePx); - GameFontManager.UnscaleFont(font, font.FontSize / mod.TargetSizePx, false); - } - - // Fill missing glyphs in MonoFont from DefaultFont - ImGuiHelpers.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); - - for (int i = 0, i_ = ioFonts.Fonts.Size; i < i_; i++) - { - var font = ioFonts.Fonts[i]; - if (font.Glyphs.Size == 0) - { - Log.Warning("[FONT] Font has no glyph: {0}", font.GetDebugName()); - continue; - } - - if (font.FindGlyphNoFallback(Fallback1Codepoint).NativePtr != null) - font.FallbackChar = Fallback1Codepoint; - - font.BuildLookupTableNonstandard(); - } - - Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); - this.AfterBuildFonts?.InvokeSafely(); - Log.Verbose("[FONT] OnAfterBuildFonts OK!"); - - if (ioFonts.Fonts[0].NativePtr != DefaultFont.NativePtr) - Log.Warning("[FONT] First font is not DefaultFont"); - - Log.Verbose("[FONT] Fonts built!"); - - this.fontBuildSignal.Set(); - - this.FontsReady = true; + if (Service.Get().WindowIsImmersive) + this.SetImmersiveMode(true); } - finally + catch (Exception ex) { - if (fontConfig.NativePtr != null) - fontConfig.Destroy(); - - foreach (var garbage in garbageList) - garbage.Free(); + Log.Error(ex, "Could not enable immersive mode"); } - } - [ServiceManager.CallWhenServicesReady( - "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] - private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration) - { - this.address.Setup(sigScanner); - this.framework.RunOnFrameworkThread(() => - { - while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) - { - _ = User32.GetWindowThreadProcessId(this.GameWindowHandle, out var pid); + this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); + this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); - if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle)) - break; - } + Log.Verbose("===== S W A P C H A I N ====="); + Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); + Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - try - { - if (configuration.WindowIsImmersive) - this.SetImmersiveMode(true); - } - catch (Exception ex) - { - Log.Error(ex, "Could not enable immersive mode"); - } - - this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); - - Log.Verbose("===== S W A P C H A I N ====="); - Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); - Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - - this.setCursorHook.Enable(); - this.presentHook.Enable(); - this.resizeBuffersHook.Enable(); - }); - } - - // This is intended to only be called as a handler attached to scene.OnNewRenderFrame - private void RebuildFontsInternal() - { - Log.Verbose("[FONT] RebuildFontsInternal() called"); - this.SetupFonts(); - - Log.Verbose("[FONT] RebuildFontsInternal() detaching"); - this.scene!.OnNewRenderFrame -= this.RebuildFontsInternal; - - Log.Verbose("[FONT] Calling InvalidateFonts"); - this.scene.InvalidateFonts(); - - Log.Verbose("[FONT] Font Rebuild OK!"); - - this.isRebuildingFonts = false; + this.setCursorHook.Enable(); + this.presentHook.Enable(); + this.resizeBuffersHook.Enable(); } private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) @@ -1206,14 +791,17 @@ internal class InterfaceManager : IDisposable, IServiceType private IntPtr SetCursorDetour(IntPtr hCursor) { - if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) + if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); + return this.setCursorHook.IsDisposed + ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() + : this.setCursorHook.Original(hCursor); } private void OnNewInputFrame() { + var io = ImGui.GetIO(); var dalamudInterface = Service.GetNullable(); var gamepadState = Service.GetNullable(); var keyState = Service.GetNullable(); @@ -1221,18 +809,21 @@ internal class InterfaceManager : IDisposable, IServiceType if (dalamudInterface == null || gamepadState == null || keyState == null) return; + // Prevent setting the footgun from ImGui Demo; the Space key isn't removing the flag at the moment. + io.ConfigFlags &= ~ImGuiConfigFlags.NoMouse; + // fix for keys in game getting stuck, if you were holding a game key (like run) // and then clicked on an imgui textbox - imgui would swallow the keyup event, // so the game would think the key remained pressed continuously until you left // imgui and pressed and released the key again - if (ImGui.GetIO().WantTextInput) + if (io.WantTextInput) { keyState.ClearAll(); } // TODO: mouse state? - var gamepadEnabled = (ImGui.GetIO().BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; + var gamepadEnabled = (io.BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; // NOTE (Chiv) Activate ImGui navigation via L1+L3 press // (mimicking how mouse navigation is activated via L1+R3 press in game). @@ -1240,12 +831,12 @@ internal class InterfaceManager : IDisposable, IServiceType && gamepadState.Raw(GamepadButtons.L1) > 0 && gamepadState.Pressed(GamepadButtons.L3) > 0) { - ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; + io.ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; gamepadState.NavEnableGamepad ^= true; dalamudInterface.ToggleGamepadModeNotifierWindow(); } - if (gamepadEnabled && (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) + if (gamepadEnabled && (io.ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) { var northButton = gamepadState.Raw(GamepadButtons.North) != 0; var eastButton = gamepadState.Raw(GamepadButtons.East) != 0; @@ -1264,7 +855,6 @@ internal class InterfaceManager : IDisposable, IServiceType var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0; var r2Button = gamepadState.Raw(GamepadButtons.R2) != 0; - var io = ImGui.GetIO(); io.AddKeyEvent(ImGuiKey.GamepadFaceUp, northButton); io.AddKeyEvent(ImGuiKey.GamepadFaceRight, eastButton); io.AddKeyEvent(ImGuiKey.GamepadFaceDown, southButton); @@ -1312,7 +902,10 @@ internal class InterfaceManager : IDisposable, IServiceType var snap = ImGuiManagedAsserts.GetSnapshot(); if (this.IsDispatchingEvents) - this.Draw?.Invoke(); + { + using (this.defaultFontHandle?.Push()) + this.Draw?.Invoke(); + } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); @@ -1339,123 +932,4 @@ internal class InterfaceManager : IDisposable, IServiceType /// public InterfaceManager Manager { get; init; } } - - /// - /// Represents a glyph request. - /// - public class SpecialGlyphRequest : IDisposable - { - /// - /// Initializes a new instance of the class. - /// - /// InterfaceManager to associate. - /// Font size in pixels. - /// Codepoint ranges. - internal SpecialGlyphRequest(InterfaceManager manager, float size, List> ranges) - { - this.Manager = manager; - this.Size = size; - this.CodepointRanges = ranges; - this.Manager.glyphRequests.Add(this); - } - - /// - /// Gets the font of specified size, or DefaultFont if it's not ready yet. - /// - public ImFontPtr Font - { - get - { - unsafe - { - return this.FontInternal.NativePtr == null ? DefaultFont : this.FontInternal; - } - } - } - - /// - /// Gets or sets the associated ImFont. - /// - internal ImFontPtr FontInternal { get; set; } - - /// - /// Gets associated InterfaceManager. - /// - internal InterfaceManager Manager { get; init; } - - /// - /// Gets font size. - /// - internal float Size { get; init; } - - /// - /// Gets codepoint ranges. - /// - internal List> CodepointRanges { get; init; } - - /// - public void Dispose() - { - this.Manager.glyphRequests.Remove(this); - } - } - - private unsafe class TargetFontModification : IDisposable - { - /// - /// Initializes a new instance of the class. - /// Constructs new target font modification information, assuming that AXIS fonts will not be applied. - /// - /// Name of the font to write to ImGui font information. - /// Target font size in pixels, which will not be considered for further scaling. - internal TargetFontModification(string name, float sizePx) - { - this.Name = name; - this.Axis = AxisMode.Suppress; - this.TargetSizePx = sizePx; - this.Scale = 1; - this.SourceAxis = null; - } - - /// - /// Initializes a new instance of the class. - /// Constructs new target font modification information. - /// - /// Name of the font to write to ImGui font information. - /// Whether and how to use AXIS fonts. - /// Target font size in pixels, which will not be considered for further scaling. - /// Font scale to be referred for loading AXIS font of appropriate size. - internal TargetFontModification(string name, AxisMode axis, float sizePx, float globalFontScale) - { - this.Name = name; - this.Axis = axis; - this.TargetSizePx = sizePx; - this.Scale = globalFontScale; - this.SourceAxis = Service.Get().NewFontRef(new(GameFontFamily.Axis, this.TargetSizePx * this.Scale)); - } - - internal enum AxisMode - { - Suppress, - GameGlyphsOnly, - Overwrite, - } - - internal string Name { get; private init; } - - internal AxisMode Axis { get; private init; } - - internal float TargetSizePx { get; private init; } - - internal float Scale { get; private init; } - - internal GameFontHandle? SourceAxis { get; private init; } - - internal bool SourceAxisAvailable => this.SourceAxis != null && this.SourceAxis.ImFont.NativePtr != null; - - public void Dispose() - { - this.SourceAxis?.Dispose(); - } - } } diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index b9e7ab686..9b0416583 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Linq; using System.Numerics; @@ -7,6 +6,8 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -31,8 +32,14 @@ internal sealed class ChangelogWindow : Window, IDisposable • Plugins can now add tooltips and interaction to the server info bar • The Dalamud/plugin installer UI has been refreshed "; - + private readonly TitleScreenMenuWindow tsmWindow; + + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private readonly Lazy bannerFont; + private readonly Lazy apiBumpExplainerTexture; + private readonly Lazy logoTexture; private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f)) { @@ -46,27 +53,36 @@ internal sealed class ChangelogWindow : Window, IDisposable Point2 = Vector2.One, }; - private IDalamudTextureWrap? apiBumpExplainerTexture; - private IDalamudTextureWrap? logoTexture; - private GameFontHandle? bannerFont; - private State state = State.WindowFadeIn; private bool needFadeRestart = false; - + /// /// Initializes a new instance of the class. /// /// TSM window. - public ChangelogWindow(TitleScreenMenuWindow tsmWindow) + /// An instance of . + /// An instance of . + public ChangelogWindow( + TitleScreenMenuWindow tsmWindow, + FontAtlasFactory fontAtlasFactory, + DalamudAssetManager assets) : base("What's new in Dalamud?##ChangelogWindow", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse, true) { this.tsmWindow = tsmWindow; this.Namespace = "DalamudChangelogWindow"; + this.privateAtlas = this.scopedFinalizer.Add( + fontAtlasFactory.CreateFontAtlas(this.Namespace, FontAtlasAutoRebuildMode.Async)); + this.bannerFont = new( + () => this.scopedFinalizer.Add( + this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.MiedingerMid18)))); + + this.apiBumpExplainerTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.ChangelogApiBumpIcon)); + this.logoTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.Logo)); // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); + _ = this.bannerFont; } private enum State @@ -97,20 +113,12 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - this.MakeFont(Service.Get()); + _ = this.bannerFont; this.state = State.WindowFadeIn; this.windowFade.Reset(); this.bodyFade.Reset(); this.needFadeRestart = true; - - if (this.apiBumpExplainerTexture == null) - { - var dalamud = Service.Get(); - var tm = Service.Get(); - this.apiBumpExplainerTexture = tm.GetTextureFromFile(new FileInfo(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "changelogApiBump.png"))) - ?? throw new Exception("Could not load api bump explainer."); - } base.OnOpen(); } @@ -186,10 +194,7 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.SetCursorPos(new Vector2(logoContainerSize.X / 2 - logoSize.X / 2, logoContainerSize.Y / 2 - logoSize.Y / 2)); using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f))) - { - this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); - ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); - } + ImGui.Image(this.logoTexture.Value.ImGuiHandle, logoSize); } ImGui.SameLine(); @@ -205,7 +210,7 @@ internal sealed class ChangelogWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f))) { - using var font = ImRaii.PushFont(this.bannerFont!.ImFont); + using var font = this.bannerFont.Value.Push(); switch (this.state) { @@ -275,9 +280,11 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.TextWrapped("If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available."); ImGuiHelpers.ScaledDummy(15); - - ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width); - ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size); + + ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture.Value.Width); + ImGui.Image( + this.apiBumpExplainerTexture.Value.ImGuiHandle, + this.apiBumpExplainerTexture.Value.Size); DrawNextButton(State.Links); break; @@ -377,7 +384,4 @@ internal sealed class ChangelogWindow : Window, IDisposable public void Dispose() { } - - private void MakeFont(GameFontManager gfm) => - this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); } diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 20c3d6d01..951d3d91c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -6,6 +6,8 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; +using Dalamud.Utility; + using ImGuiNET; using Serilog; @@ -14,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class responsible for drawing the data/debug window. /// -internal class DataWindow : Window +internal class DataWindow : Window, IDisposable { private readonly IDataWindowWidget[] modules = { @@ -34,6 +36,7 @@ internal class DataWindow : Window new FlyTextWidget(), new FontAwesomeTestWidget(), new GameInventoryTestWidget(), + new GamePrebakedFontsTestWidget(), new GamepadWidget(), new GaugeWidget(), new HookWidget(), @@ -76,6 +79,9 @@ internal class DataWindow : Window this.Load(); } + /// + public void Dispose() => this.modules.OfType().AggregateToDisposable().Dispose(); + /// public override void OnOpen() { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs new file mode 100644 index 000000000..12749114b --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -0,0 +1,186 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget for testing game prebaked fonts. +/// +internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable +{ + private ImVectorWrapper testStringBuffer; + private IFontAtlas? privateAtlas; + private IReadOnlyDictionary Handle)[]>? fontHandles; + private bool useGlobalScale; + private bool useWordWrap; + private bool useItalic; + private bool useBold; + + /// + public string[]? CommandShortcuts { get; init; } + + /// + public string DisplayName { get; init; } = "Game Prebaked Fonts"; + + /// + public bool Ready { get; set; } + + /// + public void Load() => this.Ready = true; + + /// + public unsafe void Draw() + { + ImGui.AlignTextToFramePadding(); + fixed (byte* labelPtr = "Global Scale"u8) + { + var v = (byte)(this.useGlobalScale ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useGlobalScale = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Word Wrap"u8) + { + var v = (byte)(this.useWordWrap ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + this.useWordWrap = v != 0; + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Italic"u8) + { + var v = (byte)(this.useItalic ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useItalic = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Bold"u8) + { + var v = (byte)(this.useBold ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useBold = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed) + { + this.testStringBuffer.Dispose(); + this.testStringBuffer = ImVectorWrapper.CreateFromSpan( + "(Game)-[Font] {Test}. 0123456789!! <氣気气きキ기>。"u8, + minCapacity: 1024); + } + + this.privateAtlas ??= + Service.Get().CreateFontAtlas( + nameof(GamePrebakedFontsTestWidget), + FontAtlasAutoRebuildMode.Async, + this.useGlobalScale); + this.fontHandles ??= + Enum.GetValues() + .Where(x => x.GetAttribute() is not null) + .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) + .GroupBy(x => x.Family) + .ToImmutableDictionary( + x => x.Key, + x => x.Select(y => (y, new Lazy(() => this.privateAtlas.NewGameFontHandle(y)))) + .ToArray()); + + fixed (byte* labelPtr = "Test Input"u8) + { + if (ImGuiNative.igInputTextMultiline( + labelPtr, + this.testStringBuffer.Data, + (uint)this.testStringBuffer.Capacity, + new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale), + 0, + null, + null) != 0) + { + var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); + if (len + 4 >= this.testStringBuffer.Capacity) + this.testStringBuffer.EnsureCapacityExponential(len + 4); + if (len < this.testStringBuffer.Capacity) + { + this.testStringBuffer.LengthUnsafe = len; + this.testStringBuffer.StorageSpan[len] = default; + } + } + } + + var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); + foreach (var (family, items) in this.fontHandles) + { + if (!ImGui.CollapsingHeader($"{family} Family")) + continue; + + foreach (var (gfs, handle) in items) + { + ImGui.TextUnformatted($"{gfs.SizePt}pt"); + ImGui.SameLine(offsetX); + ImGuiNative.igPushTextWrapPos(this.useWordWrap ? 0f : -1f); + try + { + if (handle.Value.LoadException is { } exc) + { + ImGui.TextUnformatted(exc.ToString()); + } + else if (!handle.Value.Available) + { + fixed (byte* labelPtr = "Loading..."u8) + ImGuiNative.igTextUnformatted(labelPtr, labelPtr + 8 + ((Environment.TickCount / 200) % 3)); + } + else + { + if (!this.useGlobalScale) + ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); + using var pushPop = handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + } + } + finally + { + ImGuiNative.igPopTextWrapPos(); + ImGuiNative.igSetWindowFontScale(1); + } + } + } + } + + /// + public void Dispose() + { + this.ClearAtlas(); + this.testStringBuffer.Dispose(); + } + + private void ClearAtlas() + { + this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value)) + .AggregateToDisposable().Dispose(); + this.fontHandles = null; + this.privateAtlas?.Dispose(); + this.privateAtlas = null; + } +} diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 7d4489f8d..414eabd22 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -8,7 +8,6 @@ using Dalamud.Interface.Internal.Windows.Settings.Tabs; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Plugin.Internal; using Dalamud.Utility; using ImGuiNET; @@ -19,14 +18,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings; /// internal class SettingsWindow : Window { - private readonly SettingsTab[] tabs = - { - new SettingsTabGeneral(), - new SettingsTabLook(), - new SettingsTabDtr(), - new SettingsTabExperimental(), - new SettingsTabAbout(), - }; + private SettingsTab[]? tabs; private string searchInput = string.Empty; @@ -49,6 +41,15 @@ internal class SettingsWindow : Window /// public override void OnOpen() { + this.tabs ??= new SettingsTab[] + { + new SettingsTabGeneral(), + new SettingsTabLook(), + new SettingsTabDtr(), + new SettingsTabExperimental(), + new SettingsTabAbout(), + }; + foreach (var settingsTab in this.tabs) { settingsTab.Load(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 5b6f6b02f..8714fd666 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -1,13 +1,13 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Numerics; using CheapLoc; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; @@ -15,7 +15,6 @@ using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; @@ -173,16 +172,21 @@ Contribute at: https://github.com/goatcorp/Dalamud "; private readonly Stopwatch creditsThrottler; + private readonly IFontAtlas privateAtlas; private string creditsText; private bool resetNow = false; private IDalamudTextureWrap? logoTexture; - private GameFontHandle? thankYouFont; + private IFontHandle? thankYouFont; public SettingsTabAbout() { this.creditsThrottler = new(); + + this.privateAtlas = Service + .Get() + .CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async); } public override SettingsEntry[] Entries { get; } = { }; @@ -207,11 +211,7 @@ Contribute at: https://github.com/goatcorp/Dalamud this.creditsThrottler.Restart(); - if (this.thankYouFont == null) - { - var gfm = Service.Get(); - this.thankYouFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.TrumpGothic34)); - } + this.thankYouFont ??= this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.TrumpGothic34)); this.resetNow = true; @@ -269,14 +269,12 @@ Contribute at: https://github.com/goatcorp/Dalamud if (this.thankYouFont != null) { - ImGui.PushFont(this.thankYouFont.ImFont); + using var fontPush = this.thankYouFont.Push(); var thankYouLenX = ImGui.CalcTextSize(ThankYouText).X; ImGui.Dummy(new Vector2((windowX / 2) - (thankYouLenX / 2), 0f)); ImGui.SameLine(); ImGui.TextUnformatted(ThankYouText); - - ImGui.PopFont(); } ImGuiHelpers.ScaledDummy(0, windowSize.Y + 50f); @@ -305,9 +303,5 @@ Contribute at: https://github.com/goatcorp/Dalamud /// /// Disposes of managed and unmanaged resources. /// - public override void Dispose() - { - this.logoTexture?.Dispose(); - this.thankYouFont?.Dispose(); - } + public override void Dispose() => this.privateAtlas.Dispose(); } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 42bca89ff..9c385a99c 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -7,11 +7,14 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; +using Dalamud.Utility; using ImGuiNET; @@ -27,16 +30,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly ClientState clientState; private readonly DalamudConfiguration configuration; - private readonly Framework framework; private readonly GameGui gameGui; private readonly TitleScreenMenu titleScreenMenu; + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private readonly Lazy myFontHandle; private readonly Lazy shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); private readonly Dictionary logoEasings = new(); - private readonly Dictionary specialGlyphRequests = new(); private InOutCubic? fadeOutEasing; @@ -48,6 +52,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// An instance of . /// An instance of . /// An instance of . + /// An instance of . /// An instance of . /// An instance of . /// An instance of . @@ -55,6 +60,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable ClientState clientState, DalamudConfiguration configuration, DalamudAssetManager dalamudAssetManager, + FontAtlasFactory fontAtlasFactory, Framework framework, GameGui gameGui, TitleScreenMenu titleScreenMenu) @@ -65,7 +71,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable { this.clientState = clientState; this.configuration = configuration; - this.framework = framework; this.gameGui = gameGui; this.titleScreenMenu = titleScreenMenu; @@ -77,9 +82,25 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); + this.privateAtlas = fontAtlasFactory.CreateFontAtlas(this.WindowName, FontAtlasAutoRebuildMode.Async); + this.scopedFinalizer.Add(this.privateAtlas); + + this.myFontHandle = new( + () => this.scopedFinalizer.Add( + this.privateAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + toolkit => toolkit.AddDalamudDefaultFont( + TargetFontSizePx, + titleScreenMenu.Entries.SelectMany(x => x.Name).ToGlyphRange()))))); + + titleScreenMenu.EntryListChange += this.TitleScreenMenuEntryListChange; + this.scopedFinalizer.Add(() => titleScreenMenu.EntryListChange -= this.TitleScreenMenuEntryListChange); + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); framework.Update += this.FrameworkOnUpdate; + this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate); } private enum State @@ -94,6 +115,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public bool AllowDrawing { get; set; } = true; + /// + public void Dispose() => this.scopedFinalizer.Dispose(); + /// public override void PreDraw() { @@ -109,12 +133,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable base.PostDraw(); } - /// - public void Dispose() - { - this.framework.Update -= this.FrameworkOnUpdate; - } - /// public override void Draw() { @@ -246,33 +264,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable break; } } - - var srcText = entries.Select(e => e.Name).ToHashSet(); - var keys = this.specialGlyphRequests.Keys.ToHashSet(); - keys.RemoveWhere(x => srcText.Contains(x)); - foreach (var key in keys) - { - this.specialGlyphRequests[key].Dispose(); - this.specialGlyphRequests.Remove(key); - } } private bool DrawEntry( TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) { - InterfaceManager.SpecialGlyphRequest fontHandle; - if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx) - { - fontHandle.Dispose(); - this.specialGlyphRequests.Remove(entry.Name); - fontHandle = null; - } - - if (fontHandle == null) - this.specialGlyphRequests[entry.Name] = fontHandle = Service.Get().NewFontSizeRef(TargetFontSizePx, entry.Name); - - ImGui.PushFont(fontHandle.Font); - ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size); + using var fontScopeDispose = this.myFontHandle.Value.Push(); var scale = ImGui.GetIO().FontGlobalScale; @@ -383,8 +380,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable initialCursor.Y += entry.Texture.Height * scale; ImGui.SetCursorPos(initialCursor); - ImGui.PopFont(); - return isHover; } @@ -401,4 +396,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero) this.IsOpen = false; } + + private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs new file mode 100644 index 000000000..50e591390 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// How to rebuild . +/// +public enum FontAtlasAutoRebuildMode +{ + /// + /// Do not rebuild. + /// + Disable, + + /// + /// Rebuild on new frame. + /// + OnNewFrame, + + /// + /// Rebuild asynchronously. + /// + Async, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs new file mode 100644 index 000000000..345ab729d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs @@ -0,0 +1,38 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Build step for . +/// +public enum FontAtlasBuildStep +{ + /// + /// An invalid value. This should never be passed through event callbacks. + /// + Invalid, + + /// + /// Called before calling .
+ /// Expect to be passed. + ///
+ PreBuild, + + /// + /// Called after calling .
+ /// Expect to be passed.
+ ///
+ /// This callback is not guaranteed to happen after , + /// but it will never happen on its own. + ///
+ PostBuild, + + /// + /// Called after promoting staging font atlas to the actual atlas for .
+ /// Expect to be passed.
+ ///
+ /// This callback is not guaranteed to happen after , + /// but it will never happen on its own. + ///
+ PostPromotion, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs new file mode 100644 index 000000000..4f5b34061 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs @@ -0,0 +1,15 @@ +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Delegate to be called when a font needs to be built. +/// +/// A toolkit that may help you for font building steps. +/// +/// An implementation of may implement all of +/// , , and +/// .
+/// Either use to identify the build step, or use +/// , , +/// and for routing. +///
+public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs new file mode 100644 index 000000000..d12409d51 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; + +using Dalamud.Interface.Utility; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Convenience function for building fonts through . +/// +public static class FontAtlasBuildToolkitUtilities +{ + /// + /// Compiles given s into an array of containing ImGui glyph ranges. + /// + /// The chars. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this IEnumerable enumerable, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); + foreach (var c in enumerable) + builder.AddChar(c); + return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); + } + + /// + /// Compiles given s into an array of containing ImGui glyph ranges. + /// + /// The chars. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this ReadOnlySpan span, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); + foreach (var c in span) + builder.AddChar(c); + return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); + } + + /// + /// Compiles given string into an array of containing ImGui glyph ranges. + /// + /// The string. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this string @string, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) => + @string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints); + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// This, for method chaining. + public static IFontAtlasBuildToolkit OnPreBuild( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PreBuild) + action.Invoke((IFontAtlasBuildToolkitPreBuild)toolkit); + return toolkit; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// toolkit, for method chaining. + public static IFontAtlasBuildToolkit OnPostBuild( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PostBuild) + action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit); + return toolkit; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// toolkit, for method chaining. + public static IFontAtlasBuildToolkit OnPostPromotion( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion) + action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit); + return toolkit; + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs new file mode 100644 index 000000000..6d971dc02 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -0,0 +1,84 @@ +using System.Threading.Tasks; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas.Internals; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Wrapper for . +/// +public interface IFontAtlas : IDisposable +{ + /// + /// Event to be called on build step changes.
+ /// is meaningless for this event. + ///
+ event FontAtlasBuildStepDelegate? BuildStepChange; + + /// + /// Event fired when a font rebuild operation is suggested.
+ /// This will be invoked from the main thread. + ///
+ event Action? RebuildRecommend; + + /// + /// Gets the name of the atlas. For logging and debugging purposes. + /// + string Name { get; } + + /// + /// Gets a value how the atlas should be rebuilt when the relevant Dalamud Configuration changes. + /// + FontAtlasAutoRebuildMode AutoRebuildMode { get; } + + /// + /// Gets the font atlas. Might be empty. + /// + ImFontAtlasPtr ImAtlas { get; } + + /// + /// Gets the task that represents the current font rebuild state. + /// + Task BuildTask { get; } + + /// + /// Gets a value indicating whether there exists any built atlas, regardless of . + /// + bool HasBuiltAtlas { get; } + + /// + /// Gets a value indicating whether this font atlas is under the effect of global scale. + /// + bool IsGlobalScaled { get; } + + /// + public IFontHandle NewGameFontHandle(GameFontStyle style); + + /// + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate @delegate); + + /// + public void FreeFontHandle(IFontHandle handle); + + /// + /// Queues rebuilding fonts, on the main thread.
+ /// Note that would not necessarily get changed from calling this function. + ///
+ void BuildFontsOnNextFrame(); + + /// + /// Rebuilds fonts immediately, on the current thread.
+ /// Even the callback for will be called on the same thread. + ///
+ void BuildFontsImmediately(); + + /// + /// Rebuilds fonts asynchronously, on any thread. + /// + /// Call on the main thread. + /// The task. + Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs new file mode 100644 index 000000000..4b016bbb2 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -0,0 +1,67 @@ +using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Common stuff for and . +/// +public interface IFontAtlasBuildToolkit +{ + /// + /// Gets or sets the font relevant to the call. + /// + ImFontPtr Font { get; set; } + + /// + /// Gets the current scale this font atlas is being built with. + /// + float Scale { get; } + + /// + /// Gets a value indicating whether the current build operation is asynchronous. + /// + bool IsAsyncBuildOperation { get; } + + /// + /// Gets the current build step. + /// + FontAtlasBuildStep BuildStep { get; } + + /// + /// Gets the font atlas being built. + /// + ImFontAtlasPtr NewImAtlas { get; } + + /// + /// Gets the wrapper for of .
+ /// This does not need to be disposed. Calling does nothing.- + ///
+ /// Modification of this vector may result in undefined behaviors. + ///
+ ImVectorWrapper Fonts { get; } + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// Disposable type. + /// The disposable. + /// The same . + T DisposeWithAtlas(T disposable) where T : IDisposable; + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// The gc handle. + /// The same . + GCHandle DisposeWithAtlas(GCHandle gcHandle); + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// The action to run on dispose. + void DisposeWithAtlas(Action action); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs new file mode 100644 index 000000000..3c14197e0 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -0,0 +1,26 @@ +using Dalamud.Interface.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is . +/// +public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit +{ + /// + /// Gets whether global scaling is ignored for the given font. + /// + /// The font. + /// True if ignored. + bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + + /// + /// Stores a texture to be managed with the atlas. + /// + /// The texture wrap. + /// Dispose the wrap on error. + /// The texture index. + int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs new file mode 100644 index 000000000..8c3c91624 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs @@ -0,0 +1,33 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is . +/// +public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit +{ + /// + /// Copies glyphs across fonts, in a safer way.
+ /// If the font does not belong to the current atlas, this function is a no-op. + ///
+ /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE'); + + /// + /// Calls , with some fixups. + /// + /// The font. + void BuildLookupTable(ImFontPtr font); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs new file mode 100644 index 000000000..e8f11aec3 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -0,0 +1,164 @@ +using System.IO; +using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is .
+///
+/// After returns, +/// either must be set, +/// or at least one font must have been added to the atlas using one of AddFont... functions. +///
+public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit +{ + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// Disposable type. + /// The disposable. + /// The same . + T DisposeAfterBuild(T disposable) where T : IDisposable; + + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// The gc handle. + /// The same . + GCHandle DisposeAfterBuild(GCHandle gcHandle); + + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// The action to run on dispose. + void DisposeAfterBuild(Action action); + + /// + /// Excludes given font from global scaling. + /// + /// The font. + /// Same with . + ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr); + + /// + /// Adds a font from memory region allocated using .
+ /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// + /// Do NOT call on the once this function has + /// been called, unless is set and the function has thrown an error. + /// + ///
+ /// Memory address for the data allocated using . + /// The size of the font file.. + /// The font config. + /// Free if an exception happens. + /// A debug tag. + /// The newly added font. + unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + nint dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag) + => this.AddFontFromImGuiHeapAllocatedMemory( + (void*)dataPointer, + dataSize, + fontConfig, + freeOnException, + debugTag); + + /// + /// Adds a font from memory region allocated using .
+ /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// Do NOT call on the once this + /// function has been called. + ///
+ /// Memory address for the data allocated using . + /// The size of the font file.. + /// The font config. + /// Free if an exception happens. + /// A debug tag. + /// The newly added font. + unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + void* dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag); + + /// + /// Adds a font from a file. + /// + /// The file path to create a new font from. + /// The font config. + /// The newly added font. + ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig); + + /// + /// Adds a font from a stream. + /// + /// The stream to create a new font from. + /// The font config. + /// Dispose when this function returns or throws. + /// A debug tag. + /// The newly added font. + ImFontPtr AddFontFromStream(Stream stream, in SafeFontConfig fontConfig, bool leaveOpen, string debugTag); + + /// + /// Adds a font from memory. + /// + /// The span to create from. + /// The font config. + /// A debug tag. + /// The newly added font. + ImFontPtr AddFontFromMemory(ReadOnlySpan span, in SafeFontConfig fontConfig, string debugTag); + + /// + /// Adds the default font known to the current font atlas.
+ ///
+ /// Default font includes and . + ///
+ /// Font size in pixels. + /// The glyph ranges. Use .ToGlyphRange to build. + /// A font returned from . + ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null); + + /// + /// Adds a font that is shipped with Dalamud.
+ ///
+ /// Note: if game symbols font file is requested but is unavailable, + /// then it will take the glyphs from game's built-in fonts, and everything in + /// will be ignored but and . + ///
+ /// The font type. + /// The font config. + /// The added font. + ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig); + + /// + /// Same with (, ...), + /// but using only FontAwesome icon ranges.
+ /// will be ignored. + ///
+ /// The font config. + /// The added font. + ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig); + + /// + /// Adds the game's symbols into the provided font.
+ /// will be ignored. + ///
+ /// The font config. + void AddGameSymbol(in SafeFontConfig fontConfig); + + /// + /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
+ /// will be ignored. + ///
+ /// The font config. + void AddExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs new file mode 100644 index 000000000..854594663 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -0,0 +1,42 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Represents a reference counting handle for fonts. +/// +public interface IFontHandle : IDisposable +{ + /// + /// Represents a reference counting handle for fonts. Dalamud internal use only. + /// + internal interface IInternal : IFontHandle + { + /// + /// Gets the font.
+ /// Use of this properly is safe only from the UI thread.
+ /// Use if the intended purpose of this property is .
+ /// Futures changes may make simple not enough. + ///
+ ImFontPtr ImFont { get; } + } + + /// + /// Gets the load exception, if it failed to load. Otherwise, it is null. + /// + Exception? LoadException { get; } + + /// + /// Gets a value indicating whether this font is ready for use.
+ /// Use directly if you want to keep the current ImGui font if the font is not ready. + ///
+ bool Available { get; } + + /// + /// Pushes the current font into ImGui font stack using , if available.
+ /// Use to access the current font.
+ /// You may not access the font once you dispose this object. + ///
+ /// A disposable object that will call (1) on dispose. + IDisposable Push(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs new file mode 100644 index 000000000..bc48ddcc1 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -0,0 +1,331 @@ +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Logging.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// A font handle representing a user-callback generated font. +/// +internal class DelegateFontHandle : IFontHandle.IInternal +{ + private IFontHandleManager? manager; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Callback for . + public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange) + { + this.manager = manager; + this.CallOnBuildStepChange = callOnBuildStepChange; + } + + /// + /// Gets the function to be called on build step changes. + /// + public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; } + + /// + public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); + + /// + public bool Available => this.ImFont.IsNotNullAndLoaded(); + + /// + public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; + + private IFontHandleManager ManagerNotDisposed => + this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); + + /// + public void Dispose() + { + this.manager?.FreeFontHandle(this); + this.manager = null; + } + + /// + public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + + /// + /// Manager for s. + /// + internal sealed class HandleManager : IFontHandleManager + { + private readonly HashSet handles = new(); + private readonly object syncRoot = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the owner atlas. + public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager"; + + /// + public event Action? RebuildRecommend; + + /// + public string Name { get; } + + /// + public IFontHandleSubstance? Substance { get; set; } + + /// + public void Dispose() + { + lock (this.syncRoot) + { + this.handles.Clear(); + this.Substance?.Dispose(); + this.Substance = null; + } + } + + /// + /// Creates a new IFontHandle using your own callbacks. + /// + /// Callback for . + /// Handle to a font that may or may not be ready yet. + public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate callOnBuildStepChange) + { + var key = new DelegateFontHandle(this, callOnBuildStepChange); + lock (this.syncRoot) + this.handles.Add(key); + this.RebuildRecommend?.Invoke(); + return key; + } + + /// + public void FreeFontHandle(IFontHandle handle) + { + if (handle is not DelegateFontHandle cgfh) + return; + + lock (this.syncRoot) + this.handles.Remove(cgfh); + } + + /// + public IFontHandleSubstance NewSubstance() + { + lock (this.syncRoot) + return new HandleSubstance(this, this.handles.ToArray()); + } + } + + /// + /// Substance from . + /// + internal sealed class HandleSubstance : IFontHandleSubstance + { + private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); + + // Not owned by this class. Do not dispose. + private readonly DelegateFontHandle[] relevantHandles; + + // Owned by this class, but ImFontPtr values still do not belong to this. + private readonly Dictionary fonts = new(); + private readonly Dictionary buildExceptions = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The manager. + /// The relevant handles. + public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) + { + this.Manager = manager; + this.relevantHandles = relevantHandles; + } + + /// + public IFontHandleManager Manager { get; } + + /// + public void Dispose() + { + this.fonts.Clear(); + this.buildExceptions.Clear(); + } + + /// + public ImFontPtr GetFontPtr(IFontHandle handle) => + handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default; + + /// + public Exception? GetBuildException(IFontHandle handle) => + handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default; + + /// + public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + var fontsVector = toolkitPreBuild.Fonts; + foreach (var k in this.relevantHandles) + { + var fontCountPrevious = fontsVector.Length; + + try + { + toolkitPreBuild.Font = default; + k.CallOnBuildStepChange(toolkitPreBuild); + if (toolkitPreBuild.Font.IsNull()) + { + if (fontCountPrevious == fontsVector.Length) + { + throw new InvalidOperationException( + $"{nameof(FontAtlasBuildStepDelegate)} must either set the " + + $"{nameof(IFontAtlasBuildToolkitPreBuild.Font)} property, or add at least one font."); + } + + toolkitPreBuild.Font = fontsVector[^1]; + } + else + { + var found = false; + unsafe + { + for (var i = fontCountPrevious; !found && i < fontsVector.Length; i++) + { + if (fontsVector[i].NativePtr == toolkitPreBuild.Font.NativePtr) + found = true; + } + } + + if (!found) + { + throw new InvalidOperationException( + "The font does not exist in the atlas' font array. If you need an empty font, try" + + "adding Noto Sans from Dalamud Assets, but using new ushort[]{ ' ', ' ', 0 } as the" + + "glyph range."); + } + } + + if (fontsVector.Length - fontCountPrevious != 1) + { + Log.Warning( + "[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " + + "did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", + this.Manager.Name, + fontsVector.Length - fontCountPrevious, + nameof(FontAtlasBuildStepDelegate), + nameof(SafeFontConfig), + nameof(SafeFontConfig.MergeFont), + nameof(ImFontConfigPtr), + nameof(ImFontConfigPtr.MergeMode)); + } + + for (var i = fontCountPrevious; i < fontsVector.Length; i++) + { + if (fontsVector[i].ValidateUnsafe() is { } ex) + { + throw new InvalidOperationException( + "One of the newly added fonts seem to be pointing to an invalid memory address.", + ex); + } + } + + // Check for duplicate entries; duplicates will result in free-after-free + for (var i = 0; i < fontCountPrevious; i++) + { + for (var j = fontCountPrevious; j < fontsVector.Length; j++) + { + unsafe + { + if (fontsVector[i].NativePtr == fontsVector[j].NativePtr) + throw new InvalidOperationException("An already added font has been added again."); + } + } + } + + this.fonts[k] = toolkitPreBuild.Font; + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}:Substance] An error has occurred while during {delegate} PreBuild call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + + // Sanitization, in a futile attempt to prevent crashes on invalid parameters + unsafe + { + var distinct = + fontsVector + .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates + .Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them + .ToArray(); + + // We're adding the contents back; do not destroy the contents + fontsVector.Clear(true); + fontsVector.AddRange(distinct.AsSpan()); + } + } + } + } + + /// + public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + foreach (var k in this.relevantHandles) + { + if (!this.fonts[k].IsNotNullAndLoaded()) + continue; + + try + { + toolkitPostBuild.Font = this.fonts[k]; + k.CallOnBuildStepChange.Invoke(toolkitPostBuild); + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}] An error has occurred while during {delegate} PostBuild call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + } + } + } + + /// + public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) + { + foreach (var k in this.relevantHandles) + { + if (!this.fonts[k].IsNotNullAndLoaded()) + continue; + + try + { + toolkitPostPromotion.Font = this.fonts[k]; + k.CallOnBuildStepChange.Invoke(toolkitPostPromotion); + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs new file mode 100644 index 000000000..9ebf20fc7 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -0,0 +1,647 @@ +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Unicode; + +using Dalamud.Configuration.Internal; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using SharpDX.DXGI; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Standalone font atlas. +/// +internal sealed partial class FontAtlasFactory +{ + private static readonly Dictionary> PairAdjustmentsCache = + new(); + + /// + /// Implementations for and + /// . + /// + private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable + { + private static readonly ushort FontAwesomeIconMin = + (ushort)Enum.GetValues().Where(x => x > 0).Min(); + + private static readonly ushort FontAwesomeIconMax = + (ushort)Enum.GetValues().Where(x => x > 0).Max(); + + private readonly DisposeSafety.ScopedFinalizer disposeAfterBuild = new(); + private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance; + private readonly FontAtlasFactory factory; + private readonly FontAtlasBuiltData data; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// New atlas. + /// An instance of . + /// Specify whether the current build operation is an asynchronous one. + public BuildToolkit( + FontAtlasFactory factory, + FontAtlasBuiltData data, + GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance, + bool isAsync) + { + this.data = data; + this.gameFontHandleSubstance = gameFontHandleSubstance; + this.IsAsyncBuildOperation = isAsync; + this.factory = factory; + } + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.data.Scale; + + /// + public bool IsAsyncBuildOperation { get; } + + /// + public FontAtlasBuildStep BuildStep { get; set; } + + /// + public ImFontAtlasPtr NewImAtlas => this.data.Atlas; + + /// + public ImVectorWrapper Fonts => this.data.Fonts; + + /// + /// Gets the list of fonts to ignore global scale. + /// + public List GlobalScaleExclusions { get; } = new(); + + /// + public void Dispose() => this.disposeAfterBuild.Dispose(); + + /// + public T2 DisposeAfterBuild(T2 disposable) where T2 : IDisposable => + this.disposeAfterBuild.Add(disposable); + + /// + public GCHandle DisposeAfterBuild(GCHandle gcHandle) => this.disposeAfterBuild.Add(gcHandle); + + /// + public void DisposeAfterBuild(Action action) => this.disposeAfterBuild.Add(action); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.data.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.data.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action); + + /// + public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) + { + this.GlobalScaleExclusions.Add(fontPtr); + return fontPtr; + } + + /// + public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => + this.GlobalScaleExclusions.Contains(fontPtr); + + /// + public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => + this.data.AddNewTexture(textureWrap, disposeOnError); + + /// + public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + void* dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag) + { + Log.Verbose( + "[{name}] 0x{atlas:X}: {funcname}(0x{dataPointer:X}, 0x{dataSize:X}, ...) from {tag}", + this.data.Owner?.Name ?? "(error)", + (nint)this.NewImAtlas.NativePtr, + nameof(this.AddFontFromImGuiHeapAllocatedMemory), + (nint)dataPointer, + dataSize, + debugTag); + + try + { + fontConfig.ThrowOnInvalidValues(); + + var raw = fontConfig.Raw with + { + FontData = dataPointer, + FontDataSize = dataSize, + }; + + if (fontConfig.GlyphRanges is not { Length: > 0 } ranges) + ranges = new ushort[] { 1, 0xFFFE, 0 }; + + raw.GlyphRanges = (ushort*)this.DisposeAfterBuild( + GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject(); + + TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw); + + var font = this.NewImAtlas.AddFont(&raw); + + var dataHash = default(HashCode); + dataHash.AddBytes(new(dataPointer, dataSize)); + var hashIdent = (uint)dataHash.ToHashCode() | ((ulong)dataSize << 32); + + List<(char Left, char Right, float Distance)> pairAdjustments; + lock (PairAdjustmentsCache) + { + if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments)) + { + PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new()); + try + { + pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray()); + } + catch + { + // don't care + } + } + } + + foreach (var pair in pairAdjustments) + { + if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Left, raw.GlyphRanges)) + continue; + if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Right, raw.GlyphRanges)) + continue; + + font.AddKerningPair(pair.Left, pair.Right, pair.Distance * raw.SizePixels); + } + + return font; + } + catch + { + if (freeOnException) + ImGuiNative.igMemFree(dataPointer); + throw; + } + } + + /// + public ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig) + { + return this.AddFontFromStream( + File.OpenRead(path), + fontConfig, + false, + $"{nameof(this.AddFontFromFile)}({path})"); + } + + /// + public unsafe ImFontPtr AddFontFromStream( + Stream stream, + in SafeFontConfig fontConfig, + bool leaveOpen, + string debugTag) + { + using var streamCloser = leaveOpen ? null : stream; + if (!stream.CanSeek) + { + // There is no need to dispose a MemoryStream. + var ms = new MemoryStream(); + stream.CopyTo(ms); + stream = ms; + } + + var length = checked((int)(uint)stream.Length); + var memory = ImGuiHelpers.AllocateMemory(length); + try + { + stream.ReadExactly(new(memory, length)); + return this.AddFontFromImGuiHeapAllocatedMemory( + memory, + length, + fontConfig, + false, + $"{nameof(this.AddFontFromStream)}({debugTag})"); + } + catch + { + ImGuiNative.igMemFree(memory); + throw; + } + } + + /// + public unsafe ImFontPtr AddFontFromMemory( + ReadOnlySpan span, + in SafeFontConfig fontConfig, + string debugTag) + { + var length = span.Length; + var memory = ImGuiHelpers.AllocateMemory(length); + try + { + span.CopyTo(new(memory, length)); + return this.AddFontFromImGuiHeapAllocatedMemory( + memory, + length, + fontConfig, + false, + $"{nameof(this.AddFontFromMemory)}({debugTag})"); + } + catch + { + ImGuiNative.igMemFree(memory); + throw; + } + } + + /// + public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges) + { + if (Service.Get().UseAxis) + { + return this.gameFontHandleSubstance.GetOrCreateFont( + new(GameFontFamily.Axis, sizePx), + this); + } + + glyphRanges ??= this.factory.DefaultGlyphRanges; + + var fontConfig = new SafeFontConfig + { + SizePx = sizePx, + GlyphRanges = glyphRanges, + }; + + var font = this.AddDalamudAssetFont(DalamudAsset.NotoSansJpMedium, fontConfig); + this.AddExtraGlyphsForDalamudLanguage(fontConfig with { MergeFont = font }); + this.AddGameSymbol(fontConfig with { MergeFont = font }); + return font; + } + + /// + public ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig) + { + if (asset.GetPurpose() != DalamudAssetPurpose.Font) + throw new ArgumentOutOfRangeException(nameof(asset), asset, "Must have the purpose of Font."); + + switch (asset) + { + case DalamudAsset.LodestoneGameSymbol when this.factory.HasGameSymbolsFontFile: + return this.factory.AddFont( + this, + asset, + fontConfig with + { + FontNo = 0, + SizePx = (fontConfig.SizePx * 3) / 2, + }); + + case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile: + return this.gameFontHandleSubstance.AttachGameSymbols( + this, + fontConfig.MergeFont, + fontConfig.SizePx); + + default: + return this.factory.AddFont( + this, + asset, + fontConfig with + { + FontNo = 0, + }); + } + } + + /// + public ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( + DalamudAsset.FontAwesomeFreeSolid, + fontConfig with + { + GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 }, + }); + + /// + public void AddGameSymbol(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( + DalamudAsset.LodestoneGameSymbol, + fontConfig with + { + GlyphRanges = new ushort[] + { + GamePrebakedFontHandle.SeIconCharMin, + GamePrebakedFontHandle.SeIconCharMax, + 0, + }, + }); + + /// + public void AddExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) + { + var dalamudConfiguration = Service.Get(); + if (dalamudConfiguration.EffectiveLanguage == "ko") + { + this.AddDalamudAssetFont( + DalamudAsset.NotoSansKrRegular, + fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.HangulJamo, + UnicodeRanges.HangulCompatibilityJamo, + UnicodeRanges.HangulSyllables, + UnicodeRanges.HangulJamoExtendedA, + UnicodeRanges.HangulJamoExtendedB), + }); + } + } + + public void PreBuildSubstances() + { + foreach (var substance in this.data.Substances) + substance.OnPreBuild(this); + } + + public unsafe void PreBuild() + { + var gamma = this.factory.InterfaceManager.FontGamma; + var configData = this.data.ConfigData; + foreach (ref var config in configData.DataSpan) + { + if (this.GlobalScaleExclusions.Contains(new(config.DstFont))) + continue; + + config.SizePixels *= this.Scale; + + config.GlyphMaxAdvanceX *= this.Scale; + if (float.IsInfinity(config.GlyphMaxAdvanceX)) + config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue; + + config.GlyphMinAdvanceX *= this.Scale; + if (float.IsInfinity(config.GlyphMinAdvanceX)) + config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue; + + config.GlyphOffset *= this.Scale; + + config.RasterizerGamma *= gamma; + } + } + + public void DoBuild() + { + // ImGui will call AddFontDefault() on Build() call. + // AddFontDefault() will reliably crash, when invoked multithreaded. + // We add a dummy font to prevent that. + if (this.data.ConfigData.Length == 0) + { + this.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 }); + } + + if (!this.NewImAtlas.Build()) + throw new InvalidOperationException("ImFontAtlas.Build failed"); + + this.BuildStep = FontAtlasBuildStep.PostBuild; + } + + public unsafe void PostBuild() + { + var scale = this.Scale; + foreach (ref var font in this.Fonts.DataSpan) + { + if (!this.GlobalScaleExclusions.Contains(font) && Math.Abs(scale - 1f) > 0f) + font.AdjustGlyphMetrics(1 / scale, scale); + + foreach (var c in FallbackCodepoints) + { + var g = font.FindGlyphNoFallback(c); + if (g.NativePtr == null) + continue; + + font.UpdateFallbackChar(c); + break; + } + + foreach (var c in EllipsisCodepoints) + { + var g = font.FindGlyphNoFallback(c); + if (g.NativePtr == null) + continue; + + font.EllipsisChar = c; + break; + } + } + } + + public void PostBuildSubstances() + { + foreach (var substance in this.data.Substances) + substance.OnPostBuild(this); + } + + public unsafe void UploadTextures() + { + var buf = Array.Empty(); + try + { + var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var bpp = use4 ? 2 : 4; + var width = this.NewImAtlas.TexWidth; + var height = this.NewImAtlas.TexHeight; + foreach (ref var texture in this.data.ImTextures.DataSpan) + { + if (texture.TexID != 0) + { + // Nothing to do + } + else if (texture.TexPixelsRGBA32 is not null) + { + var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + new(texture.TexPixelsRGBA32, width * height * 4), + width * 4, + width, + height, + use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm); + this.data.AddExistingTexture(wrap); + texture.TexID = wrap.ImGuiHandle; + } + else if (texture.TexPixelsAlpha8 is not null) + { + var numPixels = width * height; + if (buf.Length < numPixels * bpp) + { + ArrayPool.Shared.Return(buf); + buf = ArrayPool.Shared.Rent(numPixels * bpp); + } + + fixed (void* pBuf = buf) + { + var sourcePtr = texture.TexPixelsAlpha8; + if (use4) + { + var target = (ushort*)pBuf; + while (numPixels-- > 0) + { + *target = (ushort)((*sourcePtr << 8) | 0x0FFF); + target++; + sourcePtr++; + } + } + else + { + var target = (uint*)pBuf; + while (numPixels-- > 0) + { + *target = (uint)((*sourcePtr << 24) | 0x00FFFFFF); + target++; + sourcePtr++; + } + } + } + + var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + buf, + width * bpp, + width, + height, + use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm); + this.data.AddExistingTexture(wrap); + texture.TexID = wrap.ImGuiHandle; + continue; + } + else + { + Log.Warning( + "[{name}]: TexID, TexPixelsRGBA32, and TexPixelsAlpha8 are all null", + this.data.Owner?.Name ?? "(error)"); + } + + if (texture.TexPixelsRGBA32 is not null) + ImGuiNative.igMemFree(texture.TexPixelsRGBA32); + if (texture.TexPixelsAlpha8 is not null) + ImGuiNative.igMemFree(texture.TexPixelsAlpha8); + texture.TexPixelsRGBA32 = null; + texture.TexPixelsAlpha8 = null; + } + } + finally + { + ArrayPool.Shared.Return(buf); + } + } + } + + /// + /// Implementations for . + /// + private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion + { + private readonly FontAtlasBuiltData builtData; + + /// + /// Initializes a new instance of the class. + /// + /// The built data. + public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.builtData.Scale; + + /// + public bool IsAsyncBuildOperation => true; + + /// + public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; + + /// + public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; + + /// + public unsafe ImVectorWrapper Fonts => new( + &this.NewImAtlas.NativePtr->Fonts, + x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); + + /// + public unsafe void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE') + { + var sourceFound = false; + var targetFound = false; + foreach (var f in this.Fonts) + { + sourceFound |= f.NativePtr == source.NativePtr; + targetFound |= f.NativePtr == target.NativePtr; + } + + if (sourceFound && targetFound) + { + ImGuiHelpers.CopyGlyphsAcrossFonts( + source, + target, + missingOnly, + false, + rangeLow, + rangeHigh); + if (rebuildLookupTable) + this.BuildLookupTable(target); + } + } + + /// + public unsafe void BuildLookupTable(ImFontPtr font) + { + // Need to clear previous Fallback pointers before BuildLookupTable, or it may crash + font.NativePtr->FallbackGlyph = null; + font.NativePtr->FallbackHotData = null; + font.BuildLookupTable(); + + // Need to fix our custom ImGui, so that imgui_widgets.cpp:3656 stops thinking + // Codepoint < FallbackHotData.size always means that it's not fallback char. + // Otherwise, having a fallback character in ImGui.InputText gets strange. + var indexedHotData = font.IndexedHotDataWrapped(); + var indexLookup = font.IndexLookupWrapped(); + ref var fallbackHotData = ref *(ImGuiHelpers.ImFontGlyphHotDataReal*)font.NativePtr->FallbackHotData; + for (var codepoint = 0; codepoint < indexedHotData.Length; codepoint++) + { + if (indexLookup[codepoint] == ushort.MaxValue) + { + indexedHotData[codepoint].AdvanceX = fallbackHotData.AdvanceX; + indexedHotData[codepoint].OccupiedWidth = fallbackHotData.OccupiedWidth; + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs new file mode 100644 index 000000000..3f0b5b22e --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -0,0 +1,711 @@ +// #define VeryVerboseLog + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +using ImGuiNET; + +using JetBrains.Annotations; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Standalone font atlas. +/// +internal sealed partial class FontAtlasFactory +{ + /// + /// Fallback codepoints for ImFont. + /// + public const string FallbackCodepoints = "\u3013\uFFFD?-"; + + /// + /// Ellipsis codepoints for ImFont. + /// + public const string EllipsisCodepoints = "\u2026\u0085"; + + /// + /// If set, disables concurrent font build operation. + /// + private static readonly object? NoConcurrentBuildOperationLock = null; // new(); + + private static readonly ModuleLog Log = new(nameof(FontAtlasFactory)); + + private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); + + private struct FontAtlasBuiltData : IDisposable + { + public readonly DalamudFontAtlas? Owner; + public readonly ImFontAtlasPtr Atlas; + public readonly float Scale; + + public bool IsBuildInProgress; + + private readonly List? wraps; + private readonly List? substances; + private readonly DisposeSafety.ScopedFinalizer? garbage; + + public unsafe FontAtlasBuiltData( + DalamudFontAtlas owner, + IEnumerable substances, + float scale) + { + this.Owner = owner; + this.Scale = scale; + this.garbage = new(); + + try + { + var substancesList = this.substances = new(); + foreach (var s in substances) + substancesList.Add(this.garbage.Add(s)); + this.garbage.Add(() => substancesList.Clear()); + + var wrapsCopy = this.wraps = new(); + this.garbage.Add(() => wrapsCopy.Clear()); + + var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas(); + this.Atlas = atlasPtr; + if (this.Atlas.NativePtr is null) + throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}."); + + this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); + this.IsBuildInProgress = true; + } + catch + { + this.garbage.Dispose(); + throw; + } + } + + public readonly DisposeSafety.ScopedFinalizer Garbage => + this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + public readonly ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); + + public readonly ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + + public readonly ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + + public readonly IReadOnlyList Wraps => + (IReadOnlyList?)this.wraps ?? Array.Empty(); + + public readonly IReadOnlyList Substances => + (IReadOnlyList?)this.substances ?? Array.Empty(); + + public readonly void AddExistingTexture(IDalamudTextureWrap wrap) + { + if (this.wraps is null) + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + this.wraps.Add(this.Garbage.Add(wrap)); + } + + public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) + { + if (this.wraps is null) + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + var handle = wrap.ImGuiHandle; + var index = this.ImTextures.IndexOf(x => x.TexID == handle); + if (index == -1) + { + try + { + this.wraps.EnsureCapacity(this.wraps.Count + 1); + this.ImTextures.EnsureCapacityExponential(this.ImTextures.Length + 1); + + index = this.ImTextures.Length; + this.wraps.Add(this.Garbage.Add(wrap)); + this.ImTextures.Add(new() { TexID = handle }); + } + catch (Exception e) + { + if (disposeOnError) + wrap.Dispose(); + + if (this.wraps.Count != this.ImTextures.Length) + { + Log.Error( + e, + "{name} failed, and {wraps} and {imtextures} have different number of items", + nameof(this.AddNewTexture), + nameof(this.Wraps), + nameof(this.ImTextures)); + + if (this.wraps.Count > 0 && this.wraps[^1] == wrap) + this.wraps.RemoveAt(this.wraps.Count - 1); + if (this.ImTextures.Length > 0 && this.ImTextures[^1].TexID == handle) + this.ImTextures.RemoveAt(this.ImTextures.Length - 1); + + if (this.wraps.Count != this.ImTextures.Length) + Log.Fatal("^ Failed to undo due to an internal inconsistency; embrace for a crash"); + } + + throw; + } + } + + return index; + } + + public unsafe void Dispose() + { + if (this.garbage is null) + return; + + if (this.IsBuildInProgress) + { + Log.Error( + "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + + "Stack:\n{trace}", + this.Owner?.Name ?? "", + (nint)this.Atlas.NativePtr, + new StackTrace()); + while (this.IsBuildInProgress) + Thread.Sleep(100); + } + +#if VeryVerboseLog + Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); +#endif + this.garbage.Dispose(); + } + + public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) + { + var axisSubstance = this.Substances.OfType().Single(); + return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild }; + } + } + + private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback + { + private readonly DisposeSafety.ScopedFinalizer disposables = new(); + private readonly FontAtlasFactory factory; + private readonly DelegateFontHandle.HandleManager delegateFontHandleManager; + private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager; + private readonly IFontHandleManager[] fontHandleManagers; + + private readonly object syncRootPostPromotion = new(); + private readonly object syncRoot = new(); + + private Task buildTask = EmptyTask; + private FontAtlasBuiltData builtData; + + private int buildIndex; + private bool buildQueued; + private bool disposed = false; + + /// + /// Initializes a new instance of the class. + /// + /// The factory. + /// Name of atlas, for debugging and logging purposes. + /// Specify how to auto rebuild. + /// Whether the fonts in the atlas are under the effect of global scale. + public DalamudFontAtlas( + FontAtlasFactory factory, + string atlasName, + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled) + { + this.IsGlobalScaled = isGlobalScaled; + try + { + this.factory = factory; + this.AutoRebuildMode = autoRebuildMode; + this.Name = atlasName; + + this.factory.InterfaceManager.AfterBuildFonts += this.OnRebuildRecommend; + this.disposables.Add(() => this.factory.InterfaceManager.AfterBuildFonts -= this.OnRebuildRecommend); + + this.fontHandleManagers = new IFontHandleManager[] + { + this.delegateFontHandleManager = this.disposables.Add( + new DelegateFontHandle.HandleManager(atlasName)), + this.gameFontHandleManager = this.disposables.Add( + new GamePrebakedFontHandle.HandleManager(atlasName, factory)), + }; + foreach (var fhm in this.fontHandleManagers) + fhm.RebuildRecommend += this.OnRebuildRecommend; + } + catch + { + this.disposables.Dispose(); + throw; + } + + this.factory.SceneTask.ContinueWith( + r => + { + lock (this.syncRoot) + { + if (this.disposed) + return; + + r.Result.OnNewRenderFrame += this.ImGuiSceneOnNewRenderFrame; + this.disposables.Add(() => r.Result.OnNewRenderFrame -= this.ImGuiSceneOnNewRenderFrame); + } + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) + this.BuildFontsOnNextFrame(); + }); + } + + /// + /// Finalizes an instance of the class. + /// + ~DalamudFontAtlas() + { + lock (this.syncRoot) + { + this.buildTask.ToDisposableIgnoreExceptions().Dispose(); + this.builtData.Dispose(); + } + } + + /// + public event FontAtlasBuildStepDelegate? BuildStepChange; + + /// + public event Action? RebuildRecommend; + + /// + public event Action? BeforeDispose; + + /// + public event Action? AfterDispose; + + /// + public string Name { get; } + + /// + public FontAtlasAutoRebuildMode AutoRebuildMode { get; } + + /// + public ImFontAtlasPtr ImAtlas + { + get + { + lock (this.syncRoot) + return this.builtData.Atlas; + } + } + + /// + public Task BuildTask => this.buildTask; + + /// + public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); + + /// + public bool IsGlobalScaled { get; } + + /// + public void Dispose() + { + if (this.disposed) + return; + + this.BeforeDispose?.InvokeSafely(this); + + try + { + lock (this.syncRoot) + { + this.disposed = true; + this.buildTask.ToDisposableIgnoreExceptions().Dispose(); + this.buildTask = EmptyTask; + this.disposables.Add(this.builtData); + this.builtData = default; + this.disposables.Dispose(); + } + + try + { + this.AfterDispose?.Invoke(this, null); + } + catch + { + // ignore + } + } + catch (Exception e) + { + try + { + this.AfterDispose?.Invoke(this, e); + } + catch + { + // ignore + } + } + + GC.SuppressFinalize(this); + } + + /// + public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); + + /// + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate @delegate) => + this.delegateFontHandleManager.NewFontHandle(@delegate); + + /// + public void FreeFontHandle(IFontHandle handle) + { + foreach (var manager in this.fontHandleManagers) + { + manager.FreeFontHandle(handle); + } + } + + /// + public void BuildFontsOnNextFrame() + { + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsOnNextFrame)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.Async)}."); + } + + if (!this.buildTask.IsCompleted || this.buildQueued) + return; + +#if VeryVerboseLog + Log.Verbose("[{name}] Queueing from {source}.", this.Name, nameof(this.BuildFontsOnNextFrame)); +#endif + + this.buildQueued = true; + } + + /// + public void BuildFontsImmediately() + { +#if VeryVerboseLog + Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsImmediately)); +#endif + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsImmediately)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.Async)}."); + } + + var tcs = new TaskCompletionSource(); + int rebuildIndex; + try + { + rebuildIndex = ++this.buildIndex; + lock (this.syncRoot) + { + if (!this.buildTask.IsCompleted) + throw new InvalidOperationException("Font rebuild is already in progress."); + + this.buildTask = tcs.Task; + } + +#if VeryVerboseLog + Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsImmediately)); +#endif + + var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; + var r = this.RebuildFontsPrivate(false, scale); + r.Wait(); + if (r.IsCompletedSuccessfully) + tcs.SetResult(r.Result); + else if (r.Exception is not null) + tcs.SetException(r.Exception); + else + tcs.SetCanceled(); + } + catch (Exception e) + { + tcs.SetException(e); + Log.Error(e, "[{name}] Failed to build fonts.", this.Name); + throw; + } + + this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately)); + } + + /// + public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true) + { +#if VeryVerboseLog + Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync)); +#endif + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsAsync)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.OnNewFrame)}."); + } + + lock (this.syncRoot) + { + var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; + var rebuildIndex = ++this.buildIndex; + return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); + + async Task BuildInner(Task unused) + { + Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync)); + lock (this.syncRoot) + { + if (this.buildIndex != rebuildIndex) + return default; + } + + var res = await this.RebuildFontsPrivate(true, scale); + if (res.Atlas.IsNull()) + return res; + + if (callPostPromotionOnMainThread) + { + await this.factory.Framework.RunOnFrameworkThread( + () => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync))); + } + else + { + this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); + } + + return res; + } + } + } + + private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) + { + lock (this.syncRoot) + { + if (this.buildIndex != rebuildIndex) + { + data.ExplicitDisposeIgnoreExceptions(); + return; + } + + this.builtData.ExplicitDisposeIgnoreExceptions(); + this.builtData = data; + this.buildTask = EmptyTask; + foreach (var substance in data.Substances) + substance.Manager.Substance = substance; + } + + lock (this.syncRootPostPromotion) + { + if (this.buildIndex != rebuildIndex) + { + data.ExplicitDisposeIgnoreExceptions(); + return; + } + + var toolkit = new BuildToolkitPostPromotion(data); + + try + { + this.BuildStepChange?.Invoke(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {delegateName} PostPromotion error", + this.Name, + nameof(FontAtlasBuildStepDelegate)); + } + + foreach (var substance in data.Substances) + { + try + { + substance.OnPostPromotion(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {substance} PostPromotion error", + this.Name, + substance.GetType().FullName ?? substance.GetType().Name); + } + } + + foreach (var font in toolkit.Fonts) + { + try + { + toolkit.BuildLookupTable(font); + } + catch (Exception e) + { + Log.Error(e, "[{name}] BuildLookupTable error", this.Name); + } + } + +#if VeryVerboseLog + Log.Verbose("[{name}] Built from {source}.", this.Name, source); +#endif + } + } + + private void ImGuiSceneOnNewRenderFrame() + { + if (!this.buildQueued) + return; + + try + { + if (this.AutoRebuildMode != FontAtlasAutoRebuildMode.Async) + this.BuildFontsImmediately(); + } + finally + { + this.buildQueued = false; + } + } + + private Task RebuildFontsPrivate(bool isAsync, float scale) + { + if (NoConcurrentBuildOperationLock is null) + return this.RebuildFontsPrivateReal(isAsync, scale); + lock (NoConcurrentBuildOperationLock) + return this.RebuildFontsPrivateReal(isAsync, scale); + } + + private async Task RebuildFontsPrivateReal(bool isAsync, float scale) + { + lock (this.syncRoot) + { + // this lock ensures that this.buildTask is properly set. + } + + var sw = new Stopwatch(); + sw.Start(); + + var res = default(FontAtlasBuiltData); + nint atlasPtr = 0; + try + { + res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + unsafe + { + atlasPtr = (nint)res.Atlas.NativePtr; + } + + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: PreBuild (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + + using var toolkit = res.CreateToolkit(this.factory, isAsync); + this.BuildStepChange?.Invoke(toolkit); + toolkit.PreBuildSubstances(); + toolkit.PreBuild(); + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + + toolkit.DoBuild(); + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: PostBuild (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + + toolkit.PostBuild(); + toolkit.PostBuildSubstances(); + this.BuildStepChange?.Invoke(toolkit); + + if (this.factory.SceneTask is { IsCompleted: false } sceneTask) + { + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: await SceneTask (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + await sceneTask.ConfigureAwait(!isAsync); + } + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: UploadTextures (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + toolkit.UploadTextures(); + + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: Complete (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + + res.IsBuildInProgress = false; + return res; + } + catch (Exception e) + { + Log.Error( + e, + "[{name}:{functionname}] 0x{ptr:X}: Failed (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + res.IsBuildInProgress = false; + res.Dispose(); + throw; + } + finally + { + this.buildQueued = false; + } + } + + private void OnRebuildRecommend() + { + if (this.disposed) + return; + + this.factory.Framework.RunOnFrameworkThread( + () => + { + this.RebuildRecommend?.InvokeSafely(); + + switch (this.AutoRebuildMode) + { + case FontAtlasAutoRebuildMode.Async: + _ = this.BuildFontsAsync(); + break; + case FontAtlasAutoRebuildMode.OnNewFrame: + this.BuildFontsOnNextFrame(); + break; + case FontAtlasAutoRebuildMode.Disable: + default: + break; + } + }); + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs new file mode 100644 index 000000000..7a1926a9d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -0,0 +1,379 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using ImGuiScene; + +using Lumina.Data.Files; + +using SharpDX; +using SharpDX.Direct3D11; +using SharpDX.DXGI; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Factory for the implementation of . +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed partial class FontAtlasFactory + : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable +{ + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly IReadOnlyDictionary> fdtFiles; + private readonly IReadOnlyDictionary[]>> texFiles; + private readonly IReadOnlyDictionary> prebakedTextureWraps; + private readonly Task defaultGlyphRanges; + private readonly DalamudAssetManager dalamudAssetManager; + + private float lastBuildGamma = -1f; + + [ServiceManager.ServiceConstructor] + private FontAtlasFactory( + DataManager dataManager, + Framework framework, + InterfaceManager interfaceManager, + DalamudAssetManager dalamudAssetManager) + { + this.Framework = framework; + this.InterfaceManager = interfaceManager; + this.dalamudAssetManager = dalamudAssetManager; + this.SceneTask = Service + .GetAsync() + .ContinueWith(r => r.Result.Manager.Scene); + + var gffasInfo = Enum.GetValues() + .Select( + x => + ( + Font: x, + Attr: x.GetAttribute())) + .Where(x => x.Attr is not null) + .ToArray(); + var texPaths = gffasInfo.Select(x => x.Attr.TexPathFormat).Distinct().ToArray(); + + this.fdtFiles = gffasInfo.ToImmutableDictionary( + x => x.Font, + x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); + var channelCountsTask = texPaths.ToImmutableDictionary( + x => x, + x => Task.WhenAll( + gffasInfo.Where(y => y.Attr.TexPathFormat == x) + .Select(y => this.fdtFiles[y.Font])) + .ContinueWith( + files => 1 + files.Result.Max( + file => + { + unsafe + { + using var pin = file.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Length); + return fdt.MaxTextureIndex; + } + }))); + this.prebakedTextureWraps = channelCountsTask.ToImmutableDictionary( + x => x.Key, + x => x.Value.ContinueWith(y => new IDalamudTextureWrap?[y.Result])); + this.texFiles = channelCountsTask.ToImmutableDictionary( + x => x.Key, + x => x.Value.ContinueWith( + y => Enumerable + .Range(1, 1 + ((y.Result - 1) / 4)) + .Select(z => Task.Run(() => dataManager.GetFile(string.Format(x.Key, z))!)) + .ToArray())); + this.defaultGlyphRanges = + this.fdtFiles[GameFontFamilyAndSize.Axis12] + .ContinueWith( + file => + { + unsafe + { + using var pin = file.Result.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Result.Length); + return fdt.ToGlyphRanges(); + } + }); + } + + /// + /// Gets the service instance of . + /// + public Framework Framework { get; } + + /// + /// Gets the service instance of .
+ /// may not yet be available. + ///
+ public InterfaceManager InterfaceManager { get; } + + /// + /// Gets the async task for inside . + /// + public Task SceneTask { get; } + + /// + /// Gets the default glyph ranges (glyph ranges of ). + /// + public ushort[] DefaultGlyphRanges => ExtractResult(this.defaultGlyphRanges); + + /// + /// Gets a value indicating whether game symbol font file is available. + /// + public bool HasGameSymbolsFontFile => + this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol); + + /// + public void Dispose() + { + this.cancellationTokenSource.Cancel(); + this.scopedFinalizer.Dispose(); + this.cancellationTokenSource.Dispose(); + } + + /// + /// Creates a new instance of a class that implements the interface. + /// + /// Name of atlas, for debugging and logging purposes. + /// Specify how to auto rebuild. + /// Whether the fonts in the atlas is global scaled. + /// The new font atlas. + public IFontAtlas CreateFontAtlas( + string atlasName, + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled = true) => + new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled); + + /// + /// Adds the font from Dalamud Assets. + /// + /// The toolkitPostBuild. + /// The font. + /// The font config. + /// The address and size. + public ImFontPtr AddFont( + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + DalamudAsset asset, + in SafeFontConfig fontConfig) => + toolkitPreBuild.AddFontFromStream( + this.dalamudAssetManager.CreateStream(asset), + fontConfig, + false, + $"Asset({asset})"); + + /// + /// Gets the for the . + /// + /// The font family and size. + /// The . + public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); + + /// + public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) + { + var arr = ExtractResult(this.fdtFiles[gffas]); + var handle = arr.AsMemory().Pin(); + try + { + fdtFileView = new(handle.Pointer, arr.Length); + return handle; + } + catch + { + handle.Dispose(); + throw; + } + } + + /// + public int GetFontTextureCount(string texPathFormat) => + ExtractResult(this.prebakedTextureWraps[texPathFormat]).Length; + + /// + public TexFile GetTexFile(string texPathFormat, int index) => + ExtractResult(ExtractResult(this.texFiles[texPathFormat])[index]); + + /// + public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex) + { + lock (this.prebakedTextureWraps[texPathFormat]) + { + var gamma = this.InterfaceManager.FontGamma; + var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]); + if (Math.Abs(this.lastBuildGamma - gamma) > 0.0001f) + { + this.lastBuildGamma = gamma; + wraps.AggregateToDisposable().Dispose(); + wraps.AsSpan().Clear(); + } + + var fileIndex = textureIndex / 4; + var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4]; + wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex, gamma); + return CloneTextureWrap(wraps[textureIndex]); + } + } + + private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); + + private static unsafe void ExtractChannelFromB8G8R8A8( + Span target, + ReadOnlySpan source, + int channelIndex, + bool targetIsB4G4R4A4, + float gamma) + { + var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); + var gammaTable = stackalloc byte[256]; + for (var i = 0; i < 256; i++) + gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 255f, 0, 1), 1.4f / gamma) * 255); + + fixed (byte* sourcePtrImmutable = source) + { + var rptr = sourcePtrImmutable + channelIndex; + fixed (void* targetPtr = target) + { + if (targetIsB4G4R4A4) + { + var wptr = (ushort*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (ushort)((gammaTable[*rptr] << 8) | 0x0FFF); + wptr++; + rptr += 4; + } + } + else + { + var wptr = (uint*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (uint)((gammaTable[*rptr] << 24) | 0x00FFFFFF); + wptr++; + rptr += 4; + } + } + } + } + } + + /// + /// Clones a texture wrap, by getting a new reference to the underlying and the + /// texture behind. + /// + /// The to clone from. + /// The cloned . + private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) + { + var srv = CppObject.FromPointer(wrap.ImGuiHandle); + using var res = srv.Resource; + using var tex2D = res.QueryInterface(); + var description = tex2D.Description; + return new DalamudTextureWrap( + new D3DTextureWrap( + srv.QueryInterface(), + description.Width, + description.Height)); + } + + private static unsafe void ExtractChannelFromB4G4R4A4( + Span target, + ReadOnlySpan source, + int channelIndex, + bool targetIsB4G4R4A4, + float gamma) + { + var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); + fixed (byte* sourcePtrImmutable = source) + { + var rptr = sourcePtrImmutable + (channelIndex / 2); + var rshift = (channelIndex & 1) == 0 ? 0 : 4; + var gammaTable = stackalloc byte[256]; + fixed (void* targetPtr = target) + { + if (targetIsB4G4R4A4) + { + for (var i = 0; i < 16; i++) + gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 15f, 0, 1), 1.4f / gamma) * 15); + + var wptr = (ushort*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (ushort)((gammaTable[(*rptr >> rshift) & 0xF] << 12) | 0x0FFF); + wptr++; + rptr += 2; + } + } + else + { + for (var i = 0; i < 256; i++) + gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 255f, 0, 1), 1.4f / gamma) * 255); + + var wptr = (uint*)targetPtr; + while (numPixels-- > 0) + { + var v = (*rptr >> rshift) & 0xF; + v |= v << 4; + *wptr = (uint)((gammaTable[v] << 24) | 0x00FFFFFF); + wptr++; + rptr += 4; + } + } + } + } + } + + private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex, float gamma) + { + var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); + var numPixels = texFile.Header.Width * texFile.Header.Height; + + _ = Service.Get(); + var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var bpp = targetIsB4G4R4A4 ? 2 : 4; + var buffer = ArrayPool.Shared.Rent(numPixels * bpp); + try + { + var sliceSpan = texFile.SliceSpan(0, 0, out _, out _, out _); + switch (texFile.Header.Format) + { + case TexFile.TextureFormat.B4G4R4A4: + // Game ships with this format. + ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4, gamma); + break; + case TexFile.TextureFormat.B8G8R8A8: + // In case of modded font textures. + ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4, gamma); + break; + default: + // Unlikely. + ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4, gamma); + break; + } + + return this.scopedFinalizer.Add( + this.InterfaceManager.LoadImageFromDxgiFormat( + buffer, + texFile.Header.Width * bpp, + texFile.Header.Width, + texFile.Header.Height, + targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs new file mode 100644 index 000000000..012613a38 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -0,0 +1,692 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; + +using Dalamud.Game.Text; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; + +using ImGuiNET; + +using Lumina.Data.Files; + +using Vector4 = System.Numerics.Vector4; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// A font handle that uses the game's built-in fonts, optionally with some styling. +/// +internal class GamePrebakedFontHandle : IFontHandle.IInternal +{ + /// + /// The smallest value of . + /// + public static readonly char SeIconCharMin = (char)Enum.GetValues().Min(); + + /// + /// The largest value of . + /// + public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); + + private IFontHandleManager? manager; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Font to use. + public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style) + { + if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined) + throw new ArgumentOutOfRangeException(nameof(style), style, null); + + if (style.SizePt <= 0) + throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style)); + + this.manager = manager; + this.FontStyle = style; + } + + /// + /// Provider for for `common/font/fontNN.tex`. + /// + public interface IGameFontTextureProvider + { + /// + /// Creates the for the .
+ /// Dispose after use. + ///
+ /// The font family and size. + /// The view. + /// Dispose this after use.. + public MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView); + + /// + /// Gets the number of font textures. + /// + /// Format of .tex path. + /// The number of textures. + public int GetFontTextureCount(string texPathFormat); + + /// + /// Gets the for the given index of a font. + /// + /// Format of .tex path. + /// The index of .tex file. + /// The . + public TexFile GetTexFile(string texPathFormat, int index); + + /// + /// Gets a new reference of the font texture. + /// + /// Format of .tex path. + /// Texture index. + /// The texture. + public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex); + } + + /// + /// Gets the font style. + /// + public GameFontStyle FontStyle { get; } + + /// + public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); + + /// + public bool Available => this.ImFont.IsNotNullAndLoaded(); + + /// + public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; + + private IFontHandleManager ManagerNotDisposed => + this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); + + /// + public void Dispose() + { + this.manager?.FreeFontHandle(this); + this.manager = null; + } + + /// + public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + + /// + /// Manager for s. + /// + internal sealed class HandleManager : IFontHandleManager + { + private readonly Dictionary gameFontsRc = new(); + private readonly object syncRoot = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the owner atlas. + /// An instance of . + public HandleManager(string atlasName, IGameFontTextureProvider gameFontTextureProvider) + { + this.GameFontTextureProvider = gameFontTextureProvider; + this.Name = $"{atlasName}:{nameof(GamePrebakedFontHandle)}:Manager"; + } + + /// + public event Action? RebuildRecommend; + + /// + public string Name { get; } + + /// + public IFontHandleSubstance? Substance { get; set; } + + /// + /// Gets an instance of . + /// + public IGameFontTextureProvider GameFontTextureProvider { get; } + + /// + public void Dispose() + { + this.Substance?.Dispose(); + this.Substance = null; + } + + /// + /// Creates a new from game's built-in fonts. + /// + /// Font to use. + /// Handle to a font that may or may not be ready yet. + public IFontHandle NewFontHandle(GameFontStyle style) + { + var handle = new GamePrebakedFontHandle(this, style); + bool suggestRebuild; + lock (this.syncRoot) + { + this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; + suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; + } + + if (suggestRebuild) + this.RebuildRecommend?.Invoke(); + + return handle; + } + + /// + public void FreeFontHandle(IFontHandle handle) + { + if (handle is not GamePrebakedFontHandle ggfh) + return; + + lock (this.syncRoot) + { + if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) + return; + + if ((this.gameFontsRc[ggfh.FontStyle] -= 1) == 0) + this.gameFontsRc.Remove(ggfh.FontStyle); + } + } + + /// + public IFontHandleSubstance NewSubstance() + { + lock (this.syncRoot) + return new HandleSubstance(this, this.gameFontsRc.Keys); + } + } + + /// + /// Substance from . + /// + internal sealed class HandleSubstance : IFontHandleSubstance + { + private readonly HandleManager handleManager; + private readonly InterfaceManager interfaceManager; + private readonly HashSet gameFontStyles; + + // Owned by this class, but ImFontPtr values still do not belong to this. + private readonly Dictionary fonts = new(); + private readonly Dictionary buildExceptions = new(); + + private readonly Dictionary fontsSymbolsOnly = new(); + private readonly Dictionary> symbolsCopyTargets = new(); + + private readonly HashSet templatedFonts = new(); + private readonly Dictionary> lateBuildRanges = new(); + + private readonly Dictionary> glyphRectIds = + new(); + + /// + /// Initializes a new instance of the class. + /// + /// The manager. + /// The game font styles. + public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) + { + this.handleManager = manager; + this.interfaceManager = Service.Get(); + this.gameFontStyles = new(gameFontStyles); + } + + /// + public IFontHandleManager Manager => this.handleManager; + + /// + public void Dispose() + { + } + + /// + /// Attaches game symbols to the given font. + /// + /// The toolkitPostBuild. + /// The font to attach to. + /// The font size in pixels. + /// if it is not empty; otherwise a new font. + public ImFontPtr AttachGameSymbols(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, ImFontPtr font, float sizePx) + { + var style = new GameFontStyle(GameFontFamily.Axis, sizePx); + if (!this.fontsSymbolsOnly.TryGetValue(style, out var symbolFont)) + { + symbolFont = this.CreateFontPrivate(style, toolkitPreBuild, ' ', '\uFFFE', true); + this.fontsSymbolsOnly.Add(style, symbolFont); + } + + if (font.IsNull()) + font = this.CreateTemplateFont(style, toolkitPreBuild); + + if (!this.symbolsCopyTargets.TryGetValue(symbolFont, out var set)) + this.symbolsCopyTargets[symbolFont] = set = new(); + + set.Add(font); + return font; + } + + /// + /// Creates or gets a relevant for the given . + /// + /// The game font style. + /// The toolkitPostBuild. + /// The font. + public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + if (this.fonts.TryGetValue(style, out var font)) + return font; + + try + { + font = this.CreateFontPrivate(style, toolkitPreBuild, ' ', '\uFFFE', true); + this.fonts.Add(style, font); + return font; + } + catch (Exception e) + { + this.buildExceptions[style] = e; + throw; + } + } + + /// + public ImFontPtr GetFontPtr(IFontHandle handle) => + handle is GamePrebakedFontHandle ggfh ? this.fonts.GetValueOrDefault(ggfh.FontStyle) : default; + + /// + public Exception? GetBuildException(IFontHandle handle) => + handle is GamePrebakedFontHandle ggfh ? this.buildExceptions.GetValueOrDefault(ggfh.FontStyle) : default; + + /// + public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + foreach (var style in this.gameFontStyles) + { + if (this.fonts.ContainsKey(style)) + continue; + + try + { + _ = this.GetOrCreateFont(style, toolkitPreBuild); + } + catch + { + // ignore; it should have been recorded from the call + } + } + } + + /// + public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + var allTextureIndices = new Dictionary(); + var allTexFiles = new Dictionary(); + using var rentReturn = Disposable.Create( + () => + { + foreach (var x in allTextureIndices.Values) + ArrayPool.Shared.Return(x); + foreach (var x in allTexFiles.Values) + ArrayPool.Shared.Return(x); + }); + + var fontGamma = this.interfaceManager.FontGamma; + var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; + var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; + var heights = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; + for (var i = 0; i < pixels8Array.Length; i++) + toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out heights[i]); + + foreach (var (style, font) in this.fonts.Concat(this.fontsSymbolsOnly)) + { + try + { + var fas = GameFontStyle.GetRecommendedFamilyAndSize( + style.Family, + style.SizePt * toolkitPostBuild.Scale); + var attr = fas.GetAttribute(); + var horizontalOffset = attr?.HorizontalOffset ?? 0; + var texCount = this.handleManager.GameFontTextureProvider.GetFontTextureCount(attr.TexPathFormat); + using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); + ref var fdtFontHeader = ref fdt.FontHeader; + var fdtGlyphs = fdt.Glyphs; + var fontPtr = font.NativePtr; + + fontPtr->FontSize = (fdtFontHeader.Size * 4) / 3; + if (fontPtr->ConfigData != null) + fontPtr->ConfigData->SizePixels = fontPtr->FontSize; + fontPtr->Ascent = fdtFontHeader.Ascent; + fontPtr->Descent = fdtFontHeader.Descent; + fontPtr->EllipsisChar = '…'; + + if (!allTexFiles.TryGetValue(attr.TexPathFormat, out var texFiles)) + allTexFiles.Add(attr.TexPathFormat, texFiles = ArrayPool.Shared.Rent(texCount)); + + if (this.glyphRectIds.TryGetValue(style, out var rectIdToGlyphs)) + { + this.glyphRectIds.Remove(style); + + foreach (var (rectId, fdtGlyphIndex) in rectIdToGlyphs.Values) + { + ref var glyph = ref fdtGlyphs[fdtGlyphIndex]; + var rc = (ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas + .GetCustomRectByIndex(rectId) + .NativePtr; + var pixels8 = pixels8Array[rc->TextureIndex]; + var width = widths[rc->TextureIndex]; + texFiles[glyph.TextureFileIndex] ??= + this.handleManager + .GameFontTextureProvider + .GetTexFile(attr.TexPathFormat, glyph.TextureFileIndex); + var sourceBuffer = texFiles[glyph.TextureFileIndex].ImageData; + var sourceBufferDelta = glyph.TextureChannelByteIndex; + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdtFontHeader, glyph); + if (widthAdjustment == 0) + { + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var a = sourceBuffer[ + sourceBufferDelta + + (4 * (((glyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + + glyph.TextureOffsetX + x))]; + pixels8[((rc->Y + y) * width) + rc->X + x] = a; + } + } + } + else + { + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) + pixels8[((rc->Y + y) * width) + rc->X + x] = 0; + } + + for (int xbold = 0, xboldTo = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); + xbold < xboldTo; + xbold++) + { + var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); + for (var y = 0; y < glyph.BoundingHeight; y++) + { + float xDelta = xbold; + if (style.BaseSkewStrength > 0) + { + xDelta += style.BaseSkewStrength * + (fdtFontHeader.LineHeight - glyph.CurrentOffsetY - y) / + fdtFontHeader.LineHeight; + } + else if (style.BaseSkewStrength < 0) + { + xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / + fdtFontHeader.LineHeight; + } + + var xDeltaInt = (int)Math.Floor(xDelta); + var xness = xDelta - xDeltaInt; + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var sourcePixelIndex = + ((glyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + + glyph.TextureOffsetX + x; + var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; + var a2 = x == glyph.BoundingWidth - 1 + ? 0 + : sourceBuffer[sourceBufferDelta + + (4 * (sourcePixelIndex + 1))]; + var n = (a1 * xness) + (a2 * (1 - xness)); + var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; + pixels8[targetOffset] = + Math.Max(pixels8[targetOffset], (byte)(boldStrength * n)); + } + } + } + } + + if (Math.Abs(fontGamma - 1.4f) >= 0.001) + { + // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) + var xTo = rc->X + rc->Width; + var yTo = rc->Y + rc->Height; + for (int y = rc->Y; y < yTo; y++) + { + for (int x = rc->X; x < xTo; x++) + { + var i = (y * width) + x; + pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); + } + } + } + } + } + else if (this.lateBuildRanges.TryGetValue(font, out var buildRanges)) + { + buildRanges.Sort(); + for (var i = 0; i < buildRanges.Count; i++) + { + var current = buildRanges[i]; + if (current.From > current.To) + buildRanges[i] = (From: current.To, To: current.From); + } + + for (var i = 0; i < buildRanges.Count - 1; i++) + { + var current = buildRanges[i]; + var next = buildRanges[i + 1]; + if (next.From <= current.To) + { + buildRanges[i] = current with { To = next.To }; + buildRanges.RemoveAt(i + 1); + i--; + } + } + + var fdtTexSize = new Vector4( + fdtFontHeader.TextureWidth, + fdtFontHeader.TextureHeight, + fdtFontHeader.TextureWidth, + fdtFontHeader.TextureHeight); + + if (!allTextureIndices.TryGetValue(attr.TexPathFormat, out var textureIndices)) + { + allTextureIndices.Add( + attr.TexPathFormat, + textureIndices = ArrayPool.Shared.Rent(texCount)); + textureIndices.AsSpan(0, texCount).Fill(-1); + } + + var glyphs = font.GlyphsWrapped(); + glyphs.EnsureCapacity(glyphs.Length + buildRanges.Sum(x => (x.To - x.From) + 1)); + foreach (var (rangeMin, rangeMax) in buildRanges) + { + var glyphIndex = fdt.FindGlyphIndex(rangeMin); + if (glyphIndex < 0) + glyphIndex = ~glyphIndex; + var endIndex = fdt.FindGlyphIndex(rangeMax); + if (endIndex < 0) + endIndex = ~endIndex - 1; + for (; glyphIndex <= endIndex; glyphIndex++) + { + var fdtg = fdtGlyphs[glyphIndex]; + + // If the glyph already exists in the target font, we do not overwrite. + if ( + !(fdtg.Char == ' ' && this.templatedFonts.Contains(font)) + && font.FindGlyphNoFallback(fdtg.Char).NativePtr is not null) + { + continue; + } + + ref var textureIndex = ref textureIndices[fdtg.TextureIndex]; + if (textureIndex == -1) + { + textureIndex = toolkitPostBuild.StoreTexture( + this.handleManager + .GameFontTextureProvider + .NewFontTextureRef(attr.TexPathFormat, fdtg.TextureIndex), + true); + } + + var glyph = new ImGuiHelpers.ImFontGlyphReal + { + AdvanceX = fdtg.AdvanceWidth, + Codepoint = fdtg.Char, + Colored = false, + TextureIndex = textureIndex, + Visible = true, + X0 = horizontalOffset, + Y0 = fdtg.CurrentOffsetY, + U0 = fdtg.TextureOffsetX, + V0 = fdtg.TextureOffsetY, + U1 = fdtg.BoundingWidth, + V1 = fdtg.BoundingHeight, + }; + + glyph.XY1 = glyph.XY0 + glyph.UV1; + glyph.UV1 += glyph.UV0; + glyph.UV /= fdtTexSize; + + glyphs.Add(glyph); + } + } + + font.NativePtr->FallbackGlyph = null; + + font.BuildLookupTable(); + } + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); + if ((IntPtr)glyph.NativePtr != IntPtr.Zero) + { + var ptr = font.NativePtr; + ptr->FallbackChar = fallbackCharCandidate; + ptr->FallbackGlyph = glyph.NativePtr; + ptr->FallbackHotData = + (ImFontGlyphHotData*)ptr->IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + + font.AdjustGlyphMetrics(style.SizePt / fdtFontHeader.Size, toolkitPostBuild.Scale); + } + catch (Exception e) + { + this.buildExceptions[style] = e; + this.fonts[style] = default; + } + } + + foreach (var (source, targets) in this.symbolsCopyTargets) + { + foreach (var target in targets) + ImGuiHelpers.CopyGlyphsAcrossFonts(source, target, true, true, SeIconCharMin, SeIconCharMax); + } + } + + /// + public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) + { + // Irrelevant + } + + /// + /// Creates a relevant for the given . + /// + /// The game font style. + /// The toolkitPostBuild. + /// Min range. + /// Max range. + /// Add extra language glyphs. + /// The font. + private ImFontPtr CreateFontPrivate( + GameFontStyle style, + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + char minRange, + char maxRange, + bool addExtraLanguageGlyphs) + { + var font = toolkitPreBuild.IgnoreGlobalScale(this.CreateTemplateFont(style, toolkitPreBuild)); + + if (addExtraLanguageGlyphs) + toolkitPreBuild.AddExtraGlyphsForDalamudLanguage(new() { MergeFont = font }); + + var fas = GameFontStyle.GetRecommendedFamilyAndSize(style.Family, style.SizePt * toolkitPreBuild.Scale); + var horizontalOffset = fas.GetAttribute()?.HorizontalOffset ?? 0; + using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); + ref var fdtFontHeader = ref fdt.FontHeader; + var existing = new SortedSet(); + + if (style is { Bold: false, Italic: false }) + { + if (!this.lateBuildRanges.TryGetValue(font, out var ranges)) + this.lateBuildRanges[font] = ranges = new(); + + ranges.Add((minRange, maxRange)); + } + else + { + if (this.glyphRectIds.TryGetValue(style, out var rectIds)) + existing.UnionWith(rectIds.Keys); + else + rectIds = this.glyphRectIds[style] = new(); + + var glyphs = fdt.Glyphs; + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint < minRange || cint > maxRange) + continue; + + var c = (char)cint; + if (existing.Contains(c)) + continue; + + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdtFontHeader, glyph); + rectIds[c] = ( + toolkitPreBuild.NewImAtlas.AddCustomRectFontGlyph( + font, + c, + glyph.BoundingWidth + widthAdjustment, + glyph.BoundingHeight, + glyph.AdvanceWidth, + new(horizontalOffset, glyph.CurrentOffsetY)), + fdtGlyphIndex); + } + } + + foreach (ref var kernPair in fdt.PairAdjustments) + font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); + + return font; + } + + /// + /// Creates a new template font. + /// + /// The game font style. + /// The toolkitPostBuild. + /// The font. + private ImFontPtr CreateTemplateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + var font = toolkitPreBuild.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() + { + GlyphRanges = new ushort[] { ' ', ' ', '\0' }, + SizePx = style.SizePx * toolkitPreBuild.Scale, + }); + this.templatedFonts.Add(font); + return font; + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs new file mode 100644 index 000000000..795ca61fc --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -0,0 +1,34 @@ +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Manager for . +/// +internal interface IFontHandleManager : IDisposable +{ + /// + /// Event fired when a font rebuild operation is suggested. + /// + event Action? RebuildRecommend; + + /// + /// Gets the name of the font handle manager. For logging and debugging purposes. + /// + string Name { get; } + + /// + /// Gets or sets the active font handle substance. + /// + IFontHandleSubstance? Substance { get; set; } + + /// + /// Decrease font reference counter. + /// + /// Handle being released. + void FreeFontHandle(IFontHandle handle); + + /// + /// Creates a new substance of the font atlas. + /// + /// The new substance. + IFontHandleSubstance NewSubstance(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs new file mode 100644 index 000000000..fbfa2d12e --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -0,0 +1,47 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Substance of a font. +/// +internal interface IFontHandleSubstance : IDisposable +{ + /// + /// Gets the manager relevant to this instance of . + /// + IFontHandleManager Manager { get; } + + /// + /// Gets the font. + /// + /// The handle to get from. + /// Corresponding font or null. + ImFontPtr GetFontPtr(IFontHandle handle); + + /// + /// Gets the exception happened while loading for the font. + /// + /// The handle to get from. + /// Corresponding font or null. + Exception? GetBuildException(IFontHandle handle); + + /// + /// Called before call. + /// + /// The toolkit. + void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); + + /// + /// Called after call. + /// + /// The toolkit. + void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild); + + /// + /// Called on the specific thread depending on after + /// promoting the staging atlas to direct use with . + /// + /// The toolkit. + void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs new file mode 100644 index 000000000..8e7149853 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs @@ -0,0 +1,203 @@ +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + private struct Fixed : IComparable + { + public ushort Major; + public ushort Minor; + + public Fixed(ushort major, ushort minor) + { + this.Major = major; + this.Minor = minor; + } + + public Fixed(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Major); + span.ReadBig(ref offset, out this.Minor); + } + + public int CompareTo(Fixed other) + { + var majorComparison = this.Major.CompareTo(other.Major); + return majorComparison != 0 ? majorComparison : this.Minor.CompareTo(other.Minor); + } + } + + private struct KerningPair : IEquatable + { + public ushort Left; + public ushort Right; + public short Value; + + public KerningPair(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Left); + span.ReadBig(ref offset, out this.Right); + span.ReadBig(ref offset, out this.Value); + } + + public KerningPair(ushort left, ushort right, short value) + { + this.Left = left; + this.Right = right; + this.Value = value; + } + + public static bool operator ==(KerningPair left, KerningPair right) => left.Equals(right); + + public static bool operator !=(KerningPair left, KerningPair right) => !left.Equals(right); + + public static KerningPair ReverseEndianness(KerningPair pair) => new() + { + Left = BinaryPrimitives.ReverseEndianness(pair.Left), + Right = BinaryPrimitives.ReverseEndianness(pair.Right), + Value = BinaryPrimitives.ReverseEndianness(pair.Value), + }; + + public bool Equals(KerningPair other) => + this.Left == other.Left && this.Right == other.Right && this.Value == other.Value; + + public override bool Equals(object? obj) => obj is KerningPair other && this.Equals(other); + + public override int GetHashCode() => HashCode.Combine(this.Left, this.Right, this.Value); + + public override string ToString() => $"KerningPair[{this.Left}, {this.Right}] = {this.Value}"; + } + + [StructLayout(LayoutKind.Explicit, Size = 4)] + private struct PlatformAndEncoding + { + [FieldOffset(0)] + public PlatformId Platform; + + [FieldOffset(2)] + public UnicodeEncodingId UnicodeEncoding; + + [FieldOffset(2)] + public MacintoshEncodingId MacintoshEncoding; + + [FieldOffset(2)] + public IsoEncodingId IsoEncoding; + + [FieldOffset(2)] + public WindowsEncodingId WindowsEncoding; + + public PlatformAndEncoding(PointerSpan source) + { + var offset = 0; + source.ReadBig(ref offset, out this.Platform); + source.ReadBig(ref offset, out this.UnicodeEncoding); + } + + public static PlatformAndEncoding ReverseEndianness(PlatformAndEncoding value) => new() + { + Platform = (PlatformId)BinaryPrimitives.ReverseEndianness((ushort)value.Platform), + UnicodeEncoding = (UnicodeEncodingId)BinaryPrimitives.ReverseEndianness((ushort)value.UnicodeEncoding), + }; + + public readonly string Decode(Span data) + { + switch (this.Platform) + { + case PlatformId.Unicode: + switch (this.UnicodeEncoding) + { + case UnicodeEncodingId.Unicode_2_0_Bmp: + case UnicodeEncodingId.Unicode_2_0_Full: + return Encoding.BigEndianUnicode.GetString(data); + } + + break; + + case PlatformId.Macintosh: + switch (this.MacintoshEncoding) + { + case MacintoshEncodingId.Roman: + return Encoding.ASCII.GetString(data); + } + + break; + + case PlatformId.Windows: + switch (this.WindowsEncoding) + { + case WindowsEncodingId.Symbol: + case WindowsEncodingId.UnicodeBmp: + case WindowsEncodingId.UnicodeFullRepertoire: + return Encoding.BigEndianUnicode.GetString(data); + } + + break; + } + + throw new NotSupportedException(); + } + } + + [StructLayout(LayoutKind.Explicit)] + private struct TagStruct : IEquatable, IComparable + { + [FieldOffset(0)] + public unsafe fixed byte Tag[4]; + + [FieldOffset(0)] + public uint NativeValue; + + public unsafe TagStruct(char c1, char c2, char c3, char c4) + { + this.Tag[0] = checked((byte)c1); + this.Tag[1] = checked((byte)c2); + this.Tag[2] = checked((byte)c3); + this.Tag[3] = checked((byte)c4); + } + + public unsafe TagStruct(PointerSpan span) + { + this.Tag[0] = span[0]; + this.Tag[1] = span[1]; + this.Tag[2] = span[2]; + this.Tag[3] = span[3]; + } + + public unsafe TagStruct(ReadOnlySpan span) + { + this.Tag[0] = span[0]; + this.Tag[1] = span[1]; + this.Tag[2] = span[2]; + this.Tag[3] = span[3]; + } + + public unsafe byte this[int index] + { + get => this.Tag[index]; + set => this.Tag[index] = value; + } + + public static bool operator ==(TagStruct left, TagStruct right) => left.Equals(right); + + public static bool operator !=(TagStruct left, TagStruct right) => !left.Equals(right); + + public bool Equals(TagStruct other) => this.NativeValue == other.NativeValue; + + public override bool Equals(object? obj) => obj is TagStruct other && this.Equals(other); + + public override int GetHashCode() => (int)this.NativeValue; + + public int CompareTo(TagStruct other) => this.NativeValue.CompareTo(other.NativeValue); + + public override unsafe string ToString() => + $"0x{this.NativeValue:08X} \"{(char)this.Tag[0]}{(char)this.Tag[1]}{(char)this.Tag[2]}{(char)this.Tag[3]}\""; + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs new file mode 100644 index 000000000..f6a653a51 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] + private enum IsoEncodingId : ushort + { + Ascii = 0, + Iso_10646 = 1, + Iso_8859_1 = 2, + } + + private enum MacintoshEncodingId : ushort + { + Roman = 0, + } + + private enum NameId : ushort + { + CopyrightNotice = 0, + FamilyName = 1, + SubfamilyName = 2, + UniqueId = 3, + FullFontName = 4, + VersionString = 5, + PostScriptName = 6, + Trademark = 7, + Manufacturer = 8, + Designer = 9, + Description = 10, + UrlVendor = 11, + UrlDesigner = 12, + LicenseDescription = 13, + LicenseInfoUrl = 14, + TypographicFamilyName = 16, + TypographicSubfamilyName = 17, + CompatibleFullMac = 18, + SampleText = 19, + PoscSriptCidFindFontName = 20, + WwsFamilyName = 21, + WwsSubfamilyName = 22, + LightBackgroundPalette = 23, + DarkBackgroundPalette = 24, + VariationPostScriptNamePrefix = 25, + } + + private enum PlatformId : ushort + { + Unicode = 0, + Macintosh = 1, // discouraged + Iso = 2, // deprecated + Windows = 3, + Custom = 4, // OTF Windows NT compatibility mapping + } + + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] + private enum UnicodeEncodingId : ushort + { + Unicode_1_0 = 0, // deprecated + Unicode_1_1 = 1, // deprecated + IsoIec_10646 = 2, // deprecated + Unicode_2_0_Bmp = 3, + Unicode_2_0_Full = 4, + UnicodeVariationSequences = 5, + UnicodeFullRepertoire = 6, + } + + private enum WindowsEncodingId : ushort + { + Symbol = 0, + UnicodeBmp = 1, + ShiftJis = 2, + Prc = 3, + Big5 = 4, + Wansung = 5, + Johab = 6, + UnicodeFullRepertoire = 10, + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs new file mode 100644 index 000000000..3d89dd806 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs @@ -0,0 +1,148 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] +[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] +[SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1310:Field names should not contain underscore", + Justification = "Version name")] +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name")] +internal static partial class TrueTypeUtils +{ + private readonly struct SfntFile : IReadOnlyDictionary> + { + // http://formats.kaitai.io/ttf/ttf.svg + + public static readonly TagStruct FileTagTrueType1 = new('1', '\0', '\0', '\0'); + public static readonly TagStruct FileTagType1 = new('t', 'y', 'p', '1'); + public static readonly TagStruct FileTagOpenTypeWithCff = new('O', 'T', 'T', 'O'); + public static readonly TagStruct FileTagOpenType1_0 = new('\0', '\x01', '\0', '\0'); + public static readonly TagStruct FileTagTrueTypeApple = new('t', 'r', 'u', 'e'); + + public readonly PointerSpan Memory; + public readonly int OffsetInCollection; + public readonly ushort TableCount; + + public SfntFile(PointerSpan memory, int offsetInCollection = 0) + { + var span = memory.Span; + this.Memory = memory; + this.OffsetInCollection = offsetInCollection; + this.TableCount = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); + } + + public int Count => this.TableCount; + + public IEnumerable Keys => this.Select(x => x.Key); + + public IEnumerable> Values => this.Select(x => x.Value); + + public PointerSpan this[TagStruct key] => this.First(x => x.Key == key).Value; + + public IEnumerator>> GetEnumerator() + { + var offset = 12; + for (var i = 0; i < this.TableCount; i++) + { + var dte = new DirectoryTableEntry(this.Memory[offset..]); + yield return new(dte.Tag, this.Memory.Slice(dte.Offset - this.OffsetInCollection, dte.Length)); + + offset += Unsafe.SizeOf(); + } + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public bool ContainsKey(TagStruct key) => this.Any(x => x.Key == key); + + public bool TryGetValue(TagStruct key, out PointerSpan value) + { + foreach (var (k, v) in this) + { + if (k == key) + { + value = v; + return true; + } + } + + value = default; + return false; + } + + public readonly struct DirectoryTableEntry + { + public readonly PointerSpan Memory; + + public DirectoryTableEntry(PointerSpan span) => this.Memory = span; + + public TagStruct Tag => new(this.Memory); + + public uint Checksum => this.Memory.ReadU32Big(4); + + public int Offset => this.Memory.ReadI32Big(8); + + public int Length => this.Memory.ReadI32Big(12); + } + } + + private readonly struct TtcFile : IReadOnlyList + { + public static readonly TagStruct FileTag = new('t', 't', 'c', 'f'); + + public readonly PointerSpan Memory; + public readonly TagStruct Tag; + public readonly ushort MajorVersion; + public readonly ushort MinorVersion; + public readonly int FontCount; + + public TtcFile(PointerSpan memory) + { + var span = memory.Span; + this.Memory = memory; + this.Tag = new(span); + if (this.Tag != FileTag) + throw new InvalidOperationException(); + + this.MajorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); + this.MinorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[6..]); + this.FontCount = BinaryPrimitives.ReadInt32BigEndian(span[8..]); + } + + public int Count => this.FontCount; + + public SfntFile this[int index] + { + get + { + if (index < 0 || index >= this.FontCount) + { + throw new IndexOutOfRangeException( + $"The requested font #{index} does not exist in this .ttc file."); + } + + var offset = BinaryPrimitives.ReadInt32BigEndian(this.Memory.Span[(12 + 4 * index)..]); + return new(this.Memory[offset..], offset); + } + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.FontCount; i++) + yield return this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs new file mode 100644 index 000000000..d200de47b --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs @@ -0,0 +1,259 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + [Flags] + private enum LookupFlags : byte + { + RightToLeft = 1 << 0, + IgnoreBaseGlyphs = 1 << 1, + IgnoreLigatures = 1 << 2, + IgnoreMarks = 1 << 3, + UseMarkFilteringSet = 1 << 4, + } + + private enum LookupType : ushort + { + SingleAdjustment = 1, + PairAdjustment = 2, + CursiveAttachment = 3, + MarkToBaseAttachment = 4, + MarkToLigatureAttachment = 5, + MarkToMarkAttachment = 6, + ContextPositioning = 7, + ChainedContextPositioning = 8, + ExtensionPositioning = 9, + } + + private readonly struct ClassDefTable + { + public readonly PointerSpan Memory; + + public ClassDefTable(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public Format1ClassArray Format1 => new(this.Memory); + + public Format2ClassRanges Format2 => new(this.Memory); + + public IEnumerable<(ushort Class, ushort GlyphId)> Enumerate() + { + switch (this.Format) + { + case 1: + { + var format1 = this.Format1; + var startId = format1.StartGlyphId; + var count = format1.GlyphCount; + var classes = format1.ClassValueArray; + for (var i = 0; i < count; i++) + yield return (classes[i], (ushort)(i + startId)); + + break; + } + + case 2: + { + foreach (var range in this.Format2.ClassValueArray) + { + var @class = range.Class; + var startId = range.StartGlyphId; + var count = range.EndGlyphId - startId + 1; + for (var i = 0; i < count; i++) + yield return (@class, (ushort)(startId + i)); + } + + break; + } + } + } + + [Pure] + public ushort GetClass(ushort glyphId) + { + switch (this.Format) + { + case 1: + { + var format1 = this.Format1; + var startId = format1.StartGlyphId; + if (startId <= glyphId && glyphId < startId + format1.GlyphCount) + return this.Format1.ClassValueArray[glyphId - startId]; + + break; + } + + case 2: + { + var rangeSpan = this.Format2.ClassValueArray; + var i = rangeSpan.BinarySearch(new Format2ClassRanges.ClassRangeRecord { EndGlyphId = glyphId }); + if (i >= 0 && rangeSpan[i].ContainsGlyph(glyphId)) + return rangeSpan[i].Class; + + break; + } + } + + return 0; + } + + public readonly struct Format1ClassArray + { + public readonly PointerSpan Memory; + + public Format1ClassArray(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort StartGlyphId => this.Memory.ReadU16Big(2); + + public ushort GlyphCount => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan ClassValueArray => new( + this.Memory[6..].As(this.GlyphCount), + BinaryPrimitives.ReverseEndianness); + } + + public readonly struct Format2ClassRanges + { + public readonly PointerSpan Memory; + + public Format2ClassRanges(PointerSpan memory) => this.Memory = memory; + + public ushort ClassRangeCount => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan ClassValueArray => new( + this.Memory[4..].As(this.ClassRangeCount), + ClassRangeRecord.ReverseEndianness); + + public struct ClassRangeRecord : IComparable + { + public ushort StartGlyphId; + public ushort EndGlyphId; + public ushort Class; + + public static ClassRangeRecord ReverseEndianness(ClassRangeRecord value) => new() + { + StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), + EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), + Class = BinaryPrimitives.ReverseEndianness(value.Class), + }; + + public int CompareTo(ClassRangeRecord other) => this.EndGlyphId.CompareTo(other.EndGlyphId); + + public bool ContainsGlyph(ushort glyphId) => + this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; + } + } + } + + private readonly struct CoverageTable + { + public readonly PointerSpan Memory; + + public CoverageTable(PointerSpan memory) => this.Memory = memory; + + public enum CoverageFormat : ushort + { + Glyphs = 1, + RangeRecords = 2, + } + + public CoverageFormat Format => this.Memory.ReadEnumBig(0); + + public ushort Count => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan Glyphs => + this.Format == CoverageFormat.Glyphs + ? new(this.Memory[4..].As(this.Count), BinaryPrimitives.ReverseEndianness) + : default(BigEndianPointerSpan); + + public BigEndianPointerSpan RangeRecords => + this.Format == CoverageFormat.RangeRecords + ? new(this.Memory[4..].As(this.Count), RangeRecord.ReverseEndianness) + : default(BigEndianPointerSpan); + + public int GetCoverageIndex(ushort glyphId) + { + switch (this.Format) + { + case CoverageFormat.Glyphs: + return this.Glyphs.BinarySearch(glyphId); + + case CoverageFormat.RangeRecords: + { + var index = this.RangeRecords.BinarySearch( + (in RangeRecord record) => glyphId.CompareTo(record.EndGlyphId)); + + if (index >= 0 && this.RangeRecords[index].ContainsGlyph(glyphId)) + return index; + + return -1; + } + + default: + return -1; + } + } + + public struct RangeRecord + { + public ushort StartGlyphId; + public ushort EndGlyphId; + public ushort StartCoverageIndex; + + public static RangeRecord ReverseEndianness(RangeRecord value) => new() + { + StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), + EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), + StartCoverageIndex = BinaryPrimitives.ReverseEndianness(value.StartCoverageIndex), + }; + + public bool ContainsGlyph(ushort glyphId) => + this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; + } + } + + private readonly struct LookupTable : IEnumerable> + { + public readonly PointerSpan Memory; + + public LookupTable(PointerSpan memory) => this.Memory = memory; + + public LookupType Type => this.Memory.ReadEnumBig(0); + + public byte MarkAttachmentType => this.Memory[2]; + + public LookupFlags Flags => (LookupFlags)this.Memory[3]; + + public ushort SubtableCount => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan SubtableOffsets => new( + this.Memory[6..].As(this.SubtableCount), + BinaryPrimitives.ReverseEndianness); + + public PointerSpan this[int index] => this.Memory[this.SubtableOffsets[this.EnsureIndex(index)] ..]; + + public IEnumerator> GetEnumerator() + { + foreach (var i in Enumerable.Range(0, this.SubtableCount)) + yield return this.Memory[this.SubtableOffsets[i] ..]; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => index >= 0 && index < this.SubtableCount + ? index + : throw new IndexOutOfRangeException(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs new file mode 100644 index 000000000..c91df4ff2 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs @@ -0,0 +1,443 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + private delegate int BinarySearchComparer(in T value); + + private static IDisposable CreatePointerSpan(this T[] data, out PointerSpan pointerSpan) + where T : unmanaged + { + var gchandle = GCHandle.Alloc(data, GCHandleType.Pinned); + pointerSpan = new(gchandle.AddrOfPinnedObject(), data.Length); + return Disposable.Create(() => gchandle.Free()); + } + + private static int BinarySearch(this IReadOnlyList span, in T value) + where T : unmanaged, IComparable + { + var l = 0; + var r = span.Count - 1; + while (l <= r) + { + var i = (int)(((uint)r + (uint)l) >> 1); + var c = value.CompareTo(span[i]); + switch (c) + { + case 0: + return i; + case > 0: + l = i + 1; + break; + default: + r = i - 1; + break; + } + } + + return ~l; + } + + private static int BinarySearch(this IReadOnlyList span, BinarySearchComparer comparer) + where T : unmanaged + { + var l = 0; + var r = span.Count - 1; + while (l <= r) + { + var i = (int)(((uint)r + (uint)l) >> 1); + var c = comparer(span[i]); + switch (c) + { + case 0: + return i; + case > 0: + l = i + 1; + break; + default: + r = i - 1; + break; + } + } + + return ~l; + } + + private static short ReadI16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); + + private static int ReadI32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); + + private static long ReadI64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); + + private static ushort ReadU16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); + + private static uint ReadU32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); + + private static ulong ReadU64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); + + private static Half ReadF16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); + + private static float ReadF32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); + + private static double ReadF64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out short value) => + value = BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out int value) => + value = BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out long value) => + value = BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out ushort value) => + value = BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out uint value) => + value = BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out ulong value) => + value = BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out Half value) => + value = BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out float value) => + value = BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out double value) => + value = BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, ref int offset, out short value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out int value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out long value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out ushort value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out uint value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out ulong value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out Half value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out float value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out double value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static unsafe T ReadEnumBig(this PointerSpan ps, int offset) where T : unmanaged, Enum + { + switch (Marshal.SizeOf(Enum.GetUnderlyingType(typeof(T)))) + { + case 1: + var b1 = ps.Span[offset]; + return *(T*)&b1; + case 2: + var b2 = ps.ReadU16Big(offset); + return *(T*)&b2; + case 4: + var b4 = ps.ReadU32Big(offset); + return *(T*)&b4; + case 8: + var b8 = ps.ReadU64Big(offset); + return *(T*)&b8; + default: + throw new ArgumentException("Enum is not of size 1, 2, 4, or 8.", nameof(T), null); + } + } + + private static void ReadBig(this PointerSpan ps, int offset, out T value) where T : unmanaged, Enum => + value = ps.ReadEnumBig(offset); + + private static void ReadBig(this PointerSpan ps, ref int offset, out T value) where T : unmanaged, Enum + { + value = ps.ReadEnumBig(offset); + offset += Unsafe.SizeOf(); + } + + private readonly unsafe struct PointerSpan : IList, IReadOnlyList, ICollection + where T : unmanaged + { + public readonly T* Pointer; + + public PointerSpan(T* pointer, int count) + { + this.Pointer = pointer; + this.Count = count; + } + + public PointerSpan(nint pointer, int count) + : this((T*)pointer, count) + { + } + + public Span Span => new(this.Pointer, this.Count); + + public bool IsEmpty => this.Count == 0; + + public int Count { get; } + + public int Length => this.Count; + + public int ByteCount => sizeof(T) * this.Count; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => this; + + bool ICollection.IsReadOnly => false; + + public ref T this[int index] => ref this.Pointer[this.EnsureIndex(index)]; + + public PointerSpan this[Range range] => this.Slice(range.GetOffsetAndLength(this.Count)); + + T IList.this[int index] + { + get => this.Pointer[this.EnsureIndex(index)]; + set => this.Pointer[this.EnsureIndex(index)] = value; + } + + T IReadOnlyList.this[int index] => this.Pointer[this.EnsureIndex(index)]; + + public bool ContainsPointer(T2* obj) where T2 : unmanaged => + (T*)obj >= this.Pointer && (T*)(obj + 1) <= this.Pointer + this.Count; + + public PointerSpan Slice(int offset, int count) => new(this.Pointer + offset, count); + + public PointerSpan Slice((int Offset, int Count) offsetAndCount) + => this.Slice(offsetAndCount.Offset, offsetAndCount.Count); + + public PointerSpan As(int count) + where T2 : unmanaged => + count > this.Count / sizeof(T2) + ? throw new ArgumentOutOfRangeException( + nameof(count), + count, + $"Wanted {count} items; had {this.Count / sizeof(T2)} items") + : new((T2*)this.Pointer, count); + + public PointerSpan As() + where T2 : unmanaged => + new((T2*)this.Pointer, this.Count / sizeof(T2)); + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + yield return this[i]; + } + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Contains(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this.Pointer[i], item)) + return true; + } + + return false; + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array[arrayIndex + i] = this.Pointer[i]; + } + + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + int IList.IndexOf(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this.Pointer[i], item)) + return i; + } + + return -1; + } + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array.SetValue(this.Pointer[i], arrayIndex + i); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => + index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); + } + + private readonly unsafe struct BigEndianPointerSpan + : IList, IReadOnlyList, ICollection + where T : unmanaged + { + public readonly T* Pointer; + + private readonly Func reverseEndianness; + + public BigEndianPointerSpan(PointerSpan pointerSpan, Func reverseEndianness) + { + this.reverseEndianness = reverseEndianness; + this.Pointer = pointerSpan.Pointer; + this.Count = pointerSpan.Count; + } + + public int Count { get; } + + public int Length => this.Count; + + public int ByteCount => sizeof(T) * this.Count; + + public bool IsSynchronized => true; + + public object SyncRoot => this; + + public bool IsReadOnly => true; + + public T this[int index] + { + get => + BitConverter.IsLittleEndian + ? this.reverseEndianness(this.Pointer[this.EnsureIndex(index)]) + : this.Pointer[this.EnsureIndex(index)]; + set => this.Pointer[this.EnsureIndex(index)] = + BitConverter.IsLittleEndian + ? this.reverseEndianness(value) + : value; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + yield return this[i]; + } + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Contains(T item) => throw new NotSupportedException(); + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array[arrayIndex + i] = this[i]; + } + + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + int IList.IndexOf(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this[i], item)) + return i; + } + + return -1; + } + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array.SetValue(this[i], arrayIndex + i); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => + index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs new file mode 100644 index 000000000..80cf4b7da --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs @@ -0,0 +1,1391 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] +[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] +internal static partial class TrueTypeUtils +{ + [Flags] + private enum ValueFormat : ushort + { + PlacementX = 1 << 0, + PlacementY = 1 << 1, + AdvanceX = 1 << 2, + AdvanceY = 1 << 3, + PlacementDeviceOffsetX = 1 << 4, + PlacementDeviceOffsetY = 1 << 5, + AdvanceDeviceOffsetX = 1 << 6, + AdvanceDeviceOffsetY = 1 << 7, + + ValidBits = 0 + | PlacementX | PlacementY + | AdvanceX | AdvanceY + | PlacementDeviceOffsetX | PlacementDeviceOffsetY + | AdvanceDeviceOffsetX | AdvanceDeviceOffsetY, + } + + private static int NumBytes(this ValueFormat value) => + ushort.PopCount((ushort)(value & ValueFormat.ValidBits)) * 2; + + private readonly struct Cmap + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/cmap + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html + + public static readonly TagStruct DirectoryTableTag = new('c', 'm', 'a', 'p'); + + public readonly PointerSpan Memory; + + public Cmap(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Cmap(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort RecordCount => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan Records => new( + this.Memory[4..].As(this.RecordCount), + EncodingRecord.ReverseEndianness); + + public EncodingRecord? UnicodeEncodingRecord => + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Bmp }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Full }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.UnicodeFullRepertoire }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeBmp }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeFullRepertoire }); + + public CmapFormat? UnicodeTable => this.GetTable(this.UnicodeEncodingRecord); + + public CmapFormat? GetTable(EncodingRecord? encodingRecord) => + encodingRecord is { } record + ? this.Memory.ReadU16Big(record.SubtableOffset) switch + { + 0 => new CmapFormat0(this.Memory[record.SubtableOffset..]), + 2 => new CmapFormat2(this.Memory[record.SubtableOffset..]), + 4 => new CmapFormat4(this.Memory[record.SubtableOffset..]), + 6 => new CmapFormat6(this.Memory[record.SubtableOffset..]), + 8 => new CmapFormat8(this.Memory[record.SubtableOffset..]), + 10 => new CmapFormat10(this.Memory[record.SubtableOffset..]), + 12 or 13 => new CmapFormat12And13(this.Memory[record.SubtableOffset..]), + _ => null, + } + : null; + + public struct EncodingRecord + { + public PlatformAndEncoding PlatformAndEncoding; + public int SubtableOffset; + + public EncodingRecord(PointerSpan span) + { + this.PlatformAndEncoding = new(span); + var offset = Unsafe.SizeOf(); + span.ReadBig(ref offset, out this.SubtableOffset); + } + + public static EncodingRecord ReverseEndianness(EncodingRecord value) => new() + { + PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), + SubtableOffset = BinaryPrimitives.ReverseEndianness(value.SubtableOffset), + }; + } + + public struct MapGroup : IComparable + { + public int StartCharCode; + public int EndCharCode; + public int GlyphId; + + public MapGroup(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.StartCharCode); + span.ReadBig(ref offset, out this.EndCharCode); + span.ReadBig(ref offset, out this.GlyphId); + } + + public static MapGroup ReverseEndianness(MapGroup obj) => new() + { + StartCharCode = BinaryPrimitives.ReverseEndianness(obj.StartCharCode), + EndCharCode = BinaryPrimitives.ReverseEndianness(obj.EndCharCode), + GlyphId = BinaryPrimitives.ReverseEndianness(obj.GlyphId), + }; + + public int CompareTo(MapGroup other) + { + var endCharCodeComparison = this.EndCharCode.CompareTo(other.EndCharCode); + if (endCharCodeComparison != 0) return endCharCodeComparison; + + var startCharCodeComparison = this.StartCharCode.CompareTo(other.StartCharCode); + if (startCharCodeComparison != 0) return startCharCodeComparison; + + return this.GlyphId.CompareTo(other.GlyphId); + } + } + + public abstract class CmapFormat : IReadOnlyDictionary + { + public int Count => this.Count(x => x.Value != 0); + + public IEnumerable Keys => this.Select(x => x.Key); + + public IEnumerable Values => this.Select(x => x.Value); + + public ushort this[int key] => throw new NotImplementedException(); + + public abstract ushort CharToGlyph(int c); + + public abstract IEnumerator> GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public bool ContainsKey(int key) => this.CharToGlyph(key) != 0; + + public bool TryGetValue(int key, out ushort value) + { + value = this.CharToGlyph(key); + return value != 0; + } + } + + public class CmapFormat0 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat0(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public PointerSpan GlyphIdArray => this.Memory.Slice(6, 256); + + public override ushort CharToGlyph(int c) => c is >= 0 and < 256 ? this.GlyphIdArray[c] : (byte)0; + + public override IEnumerator> GetEnumerator() + { + for (var codepoint = 0; codepoint < 256; codepoint++) + { + if (this.GlyphIdArray[codepoint] is var glyphId and not 0) + yield return new(codepoint, glyphId); + } + } + } + + public class CmapFormat2 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat2(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan SubHeaderKeys => new( + this.Memory[6..].As(256), + BinaryPrimitives.ReverseEndianness); + + public PointerSpan Data => this.Memory[518..]; + + public bool TryGetSubHeader( + int keyIndex, out SubHeader subheader, out BigEndianPointerSpan glyphSpan) + { + if (keyIndex < 0 || keyIndex >= this.SubHeaderKeys.Count) + { + subheader = default; + glyphSpan = default; + return false; + } + + var offset = this.SubHeaderKeys[keyIndex]; + if (offset + Unsafe.SizeOf() > this.Data.Length) + { + subheader = default; + glyphSpan = default; + return false; + } + + subheader = new(this.Data[offset..]); + glyphSpan = new( + this.Data[(offset + Unsafe.SizeOf() + subheader.IdRangeOffset)..] + .As(subheader.EntryCount), + BinaryPrimitives.ReverseEndianness); + + return true; + } + + public override ushort CharToGlyph(int c) + { + if (!this.TryGetSubHeader(c >> 8, out var sh, out var glyphSpan)) + return 0; + + c = (c & 0xFF) - sh.FirstCode; + if (c > 0 || c >= glyphSpan.Count) + return 0; + + var res = glyphSpan[c]; + return res == 0 ? (ushort)0 : unchecked((ushort)(res + sh.IdDelta)); + } + + public override IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.SubHeaderKeys.Count; i++) + { + if (!this.TryGetSubHeader(i, out var sh, out var glyphSpan)) + continue; + + for (var j = 0; j < glyphSpan.Count; j++) + { + var res = glyphSpan[j]; + if (res == 0) + continue; + + var glyphId = unchecked((ushort)(res + sh.IdDelta)); + if (glyphId == 0) + continue; + + var codepoint = (i << 8) | (sh.FirstCode + j); + yield return new(codepoint, glyphId); + } + } + } + + public struct SubHeader + { + public ushort FirstCode; + public ushort EntryCount; + public ushort IdDelta; + public ushort IdRangeOffset; + + public SubHeader(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.FirstCode); + span.ReadBig(ref offset, out this.EntryCount); + span.ReadBig(ref offset, out this.IdDelta); + span.ReadBig(ref offset, out this.IdRangeOffset); + } + } + } + + public class CmapFormat4 : CmapFormat + { + public const int EndCodesOffset = 14; + + public readonly PointerSpan Memory; + + public CmapFormat4(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public ushort SegCountX2 => this.Memory.ReadU16Big(6); + + public ushort SearchRange => this.Memory.ReadU16Big(8); + + public ushort EntrySelector => this.Memory.ReadU16Big(10); + + public ushort RangeShift => this.Memory.ReadU16Big(12); + + public BigEndianPointerSpan EndCodes => new( + this.Memory.Slice(EndCodesOffset, this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan StartCodes => new( + this.Memory.Slice(EndCodesOffset + 2 + (1 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan IdDeltas => new( + this.Memory.Slice(EndCodesOffset + 2 + (2 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan IdRangeOffsets => new( + this.Memory.Slice(EndCodesOffset + 2 + (3 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan GlyphIds => new( + this.Memory.Slice(EndCodesOffset + 2 + (4 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + if (c is < 0 or >= 0x10000) + return 0; + + var i = this.EndCodes.BinarySearch((ushort)c); + if (i < 0) + return 0; + + var startCode = this.StartCodes[i]; + var endCode = this.EndCodes[i]; + if (c < startCode || c > endCode) + return 0; + + var idRangeOffset = this.IdRangeOffsets[i]; + var idDelta = this.IdDeltas[i]; + if (idRangeOffset == 0) + return unchecked((ushort)(c + idDelta)); + + var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; + if (ptr > this.Memory.Length) + return 0; + + var glyphs = new BigEndianPointerSpan( + this.Memory[ptr..].As(endCode - startCode + 1), + BinaryPrimitives.ReverseEndianness); + + var glyph = glyphs[c - startCode]; + return unchecked(glyph == 0 ? (ushort)0 : (ushort)(idDelta + glyph)); + } + + public override IEnumerator> GetEnumerator() + { + var startCodes = this.StartCodes; + var endCodes = this.EndCodes; + var idDeltas = this.IdDeltas; + var idRangeOffsets = this.IdRangeOffsets; + + for (var i = 0; i < this.SegCountX2 / 2; i++) + { + var startCode = startCodes[i]; + var endCode = endCodes[i]; + var idRangeOffset = idRangeOffsets[i]; + var idDelta = idDeltas[i]; + + if (idRangeOffset == 0) + { + for (var c = (int)startCode; c <= endCode; c++) + yield return new(c, (ushort)(c + idDelta)); + } + else + { + var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; + if (ptr >= this.Memory.Length) + continue; + + var glyphs = new BigEndianPointerSpan( + this.Memory[ptr..].As(endCode - startCode + 1), + BinaryPrimitives.ReverseEndianness); + + for (var j = 0; j < glyphs.Count; j++) + { + var glyphId = glyphs[j]; + if (glyphId == 0) + continue; + + glyphId += idDelta; + if (glyphId == 0) + continue; + + yield return new(startCode + j, glyphId); + } + } + } + } + } + + public class CmapFormat6 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat6(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public ushort FirstCode => this.Memory.ReadU16Big(6); + + public ushort EntryCount => this.Memory.ReadU16Big(8); + + public BigEndianPointerSpan GlyphIds => new( + this.Memory[10..].As(this.EntryCount), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var glyphIds = this.GlyphIds; + if (c < this.FirstCode || c >= this.FirstCode + this.GlyphIds.Count) + return 0; + + return glyphIds[c - this.FirstCode]; + } + + public override IEnumerator> GetEnumerator() + { + var glyphIds = this.GlyphIds; + for (var i = 0; i < this.GlyphIds.Length; i++) + { + var g = glyphIds[i]; + if (g != 0) + yield return new(this.FirstCode + i, g); + } + } + } + + public class CmapFormat8 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat8(PointerSpan memory) => this.Memory = memory; + + public int Format => this.Memory.ReadI32Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public PointerSpan Is32 => this.Memory.Slice(12, 8192); + + public int NumGroups => this.Memory.ReadI32Big(8204); + + public BigEndianPointerSpan Groups => + new(this.Memory[8208..].As(), MapGroup.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var groups = this.Groups; + + var i = groups.BinarySearch((in MapGroup value) => c.CompareTo(value.EndCharCode)); + if (i < 0) + return 0; + + var group = groups[i]; + if (c < group.StartCharCode || c > group.EndCharCode) + return 0; + + return unchecked((ushort)(group.GlyphId + c - group.StartCharCode)); + } + + public override IEnumerator> GetEnumerator() + { + foreach (var group in this.Groups) + { + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + { + var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); + if (glyphId == 0) + continue; + + yield return new(j, glyphId); + } + } + } + } + + public class CmapFormat10 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat10(PointerSpan memory) => this.Memory = memory; + + public int Format => this.Memory.ReadI32Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public int StartCharCode => this.Memory.ReadI32Big(12); + + public int NumChars => this.Memory.ReadI32Big(16); + + public BigEndianPointerSpan GlyphIdArray => new( + this.Memory.Slice(20, this.NumChars * 2).As(), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + if (c < this.StartCharCode || c >= this.StartCharCode + this.GlyphIdArray.Count) + return 0; + + return this.GlyphIdArray[c]; + } + + public override IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.GlyphIdArray.Count; i++) + { + var glyph = this.GlyphIdArray[i]; + if (glyph != 0) + yield return new(this.StartCharCode + i, glyph); + } + } + } + + public class CmapFormat12And13 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat12And13(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public int NumGroups => this.Memory.ReadI32Big(12); + + public BigEndianPointerSpan Groups => new( + this.Memory[16..].As(this.NumGroups), + MapGroup.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var groups = this.Groups; + + var i = groups.BinarySearch(new MapGroup() { EndCharCode = c }); + if (i < 0) + return 0; + + var group = groups[i]; + if (c < group.StartCharCode || c > group.EndCharCode) + return 0; + + if (this.Format == 12) + return (ushort)(group.GlyphId + c - group.StartCharCode); + else + return (ushort)group.GlyphId; + } + + public override IEnumerator> GetEnumerator() + { + var groups = this.Groups; + if (this.Format == 12) + { + foreach (var group in groups) + { + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + { + var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); + if (glyphId == 0) + continue; + + yield return new(j, glyphId); + } + } + } + else + { + foreach (var group in groups) + { + if (group.GlyphId == 0) + continue; + + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + yield return new(j, (ushort)group.GlyphId); + } + } + } + } + } + + private readonly struct Gpos + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos + + public static readonly TagStruct DirectoryTableTag = new('G', 'P', 'O', 'S'); + + public readonly PointerSpan Memory; + + public Gpos(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Gpos(PointerSpan memory) => this.Memory = memory; + + public Fixed Version => new(this.Memory); + + public ushort ScriptListOffset => this.Memory.ReadU16Big(4); + + public ushort FeatureListOffset => this.Memory.ReadU16Big(6); + + public ushort LookupListOffset => this.Memory.ReadU16Big(8); + + public uint FeatureVariationsOffset => this.Version.CompareTo(new(1, 1)) >= 0 + ? this.Memory.ReadU32Big(10) + : 0; + + public BigEndianPointerSpan LookupOffsetList => new( + this.Memory[(this.LookupListOffset + 2)..].As( + this.Memory.ReadU16Big(this.LookupListOffset)), + BinaryPrimitives.ReverseEndianness); + + public IEnumerable EnumerateLookupTables() + { + foreach (var offset in this.LookupOffsetList) + yield return new(this.Memory[(this.LookupListOffset + offset)..]); + } + + public IEnumerable ExtractAdvanceX() => + this.EnumerateLookupTables() + .SelectMany( + lookupTable => lookupTable.Type switch + { + LookupType.PairAdjustment => + lookupTable.SelectMany(y => new PairAdjustmentPositioning(y).ExtractAdvanceX()), + LookupType.ExtensionPositioning => + lookupTable + .Where(y => y.ReadU16Big(0) == 1) + .Select(y => new ExtensionPositioningSubtableFormat1(y)) + .Where(y => y.ExtensionLookupType == LookupType.PairAdjustment) + .SelectMany(y => new PairAdjustmentPositioning(y.ExtensionData).ExtractAdvanceX()), + _ => Array.Empty(), + }); + + public struct ValueRecord + { + public short PlacementX; + public short PlacementY; + public short AdvanceX; + public short AdvanceY; + public short PlacementDeviceOffsetX; + public short PlacementDeviceOffsetY; + public short AdvanceDeviceOffsetX; + public short AdvanceDeviceOffsetY; + + public ValueRecord(PointerSpan pointerSpan, ValueFormat valueFormat) + { + var offset = 0; + if ((valueFormat & ValueFormat.PlacementX) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementX); + + if ((valueFormat & ValueFormat.PlacementY) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementY); + + if ((valueFormat & ValueFormat.AdvanceX) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceX); + if ((valueFormat & ValueFormat.AdvanceY) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceY); + if ((valueFormat & ValueFormat.PlacementDeviceOffsetX) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetX); + + if ((valueFormat & ValueFormat.PlacementDeviceOffsetY) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetY); + + if ((valueFormat & ValueFormat.AdvanceDeviceOffsetX) != 0) + pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetX); + + if ((valueFormat & ValueFormat.AdvanceDeviceOffsetY) != 0) + pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetY); + } + } + + public readonly struct PairAdjustmentPositioning + { + public readonly PointerSpan Memory; + + public PairAdjustmentPositioning(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public IEnumerable ExtractAdvanceX() => this.Format switch + { + 1 => new Format1(this.Memory).ExtractAdvanceX(), + 2 => new Format2(this.Memory).ExtractAdvanceX(), + _ => Array.Empty(), + }; + + public readonly struct Format1 + { + public readonly PointerSpan Memory; + + public Format1(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort CoverageOffset => this.Memory.ReadU16Big(2); + + public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); + + public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); + + public ushort PairSetCount => this.Memory.ReadU16Big(8); + + public BigEndianPointerSpan PairSetOffsets => new( + this.Memory[10..].As(this.PairSetCount), + BinaryPrimitives.ReverseEndianness); + + public CoverageTable CoverageTable => new(this.Memory[this.CoverageOffset..]); + + public PairSet this[int index] => new( + this.Memory[this.PairSetOffsets[index] ..], + this.ValueFormat1, + this.ValueFormat2); + + public IEnumerable ExtractAdvanceX() + { + if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && + (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) + { + yield break; + } + + var coverageTable = this.CoverageTable; + switch (coverageTable.Format) + { + case CoverageTable.CoverageFormat.Glyphs: + { + var glyphSpan = coverageTable.Glyphs; + foreach (var coverageIndex in Enumerable.Range(0, glyphSpan.Count)) + { + var glyph1Id = glyphSpan[coverageIndex]; + PairSet pairSetView; + try + { + pairSetView = this[coverageIndex]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) + { + var pair = pairSetView[pairIndex]; + var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); + if (adj >= 10000) + System.Diagnostics.Debugger.Break(); + + if (adj != 0) + yield return new(glyph1Id, pair.SecondGlyph, adj); + } + } + + break; + } + + case CoverageTable.CoverageFormat.RangeRecords: + { + foreach (var rangeRecord in coverageTable.RangeRecords) + { + var startGlyphId = rangeRecord.StartGlyphId; + var endGlyphId = rangeRecord.EndGlyphId; + var startCoverageIndex = rangeRecord.StartCoverageIndex; + var glyphCount = endGlyphId - startGlyphId + 1; + foreach (var glyph1Id in Enumerable.Range(startGlyphId, glyphCount)) + { + PairSet pairSetView; + try + { + pairSetView = this[startCoverageIndex + glyph1Id - startGlyphId]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) + { + var pair = pairSetView[pairIndex]; + var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); + if (adj != 0) + yield return new((ushort)glyph1Id, pair.SecondGlyph, adj); + } + } + } + + break; + } + } + } + + public readonly struct PairSet + { + public readonly PointerSpan Memory; + public readonly ValueFormat ValueFormat1; + public readonly ValueFormat ValueFormat2; + public readonly int PairValue1Size; + public readonly int PairValue2Size; + public readonly int PairSize; + + public PairSet( + PointerSpan memory, + ValueFormat valueFormat1, + ValueFormat valueFormat2) + { + this.Memory = memory; + this.ValueFormat1 = valueFormat1; + this.ValueFormat2 = valueFormat2; + this.PairValue1Size = this.ValueFormat1.NumBytes(); + this.PairValue2Size = this.ValueFormat2.NumBytes(); + this.PairSize = 2 + this.PairValue1Size + this.PairValue2Size; + } + + public ushort Count => this.Memory.ReadU16Big(0); + + public PairValueRecord this[int index] + { + get + { + var pvr = this.Memory.Slice(2 + (this.PairSize * index), this.PairSize); + return new() + { + SecondGlyph = pvr.ReadU16Big(0), + Record1 = new(pvr.Slice(2, this.PairValue1Size), this.ValueFormat1), + Record2 = new( + pvr.Slice(2 + this.PairValue1Size, this.PairValue2Size), + this.ValueFormat2), + }; + } + } + + public struct PairValueRecord + { + public ushort SecondGlyph; + public ValueRecord Record1; + public ValueRecord Record2; + } + } + } + + public readonly struct Format2 + { + public readonly PointerSpan Memory; + public readonly int PairValue1Size; + public readonly int PairValue2Size; + public readonly int PairSize; + + public Format2(PointerSpan memory) + { + this.Memory = memory; + this.PairValue1Size = this.ValueFormat1.NumBytes(); + this.PairValue2Size = this.ValueFormat2.NumBytes(); + this.PairSize = this.PairValue1Size + this.PairValue2Size; + } + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort CoverageOffset => this.Memory.ReadU16Big(2); + + public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); + + public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); + + public ushort ClassDef1Offset => this.Memory.ReadU16Big(8); + + public ushort ClassDef2Offset => this.Memory.ReadU16Big(10); + + public ushort Class1Count => this.Memory.ReadU16Big(12); + + public ushort Class2Count => this.Memory.ReadU16Big(14); + + public ClassDefTable ClassDefTable1 => new(this.Memory[this.ClassDef1Offset..]); + + public ClassDefTable ClassDefTable2 => new(this.Memory[this.ClassDef2Offset..]); + + public (ValueRecord Record1, ValueRecord Record2) this[(int Class1Index, int Class2Index) v] => + this[v.Class1Index, v.Class2Index]; + + public (ValueRecord Record1, ValueRecord Record2) this[int class1Index, int class2Index] + { + get + { + if (class1Index < 0 || class1Index >= this.Class1Count) + throw new IndexOutOfRangeException(); + + if (class2Index < 0 || class2Index >= this.Class2Count) + throw new IndexOutOfRangeException(); + + var offset = 16 + (this.PairSize * ((class1Index * this.Class2Count) + class2Index)); + return ( + new(this.Memory.Slice(offset, this.PairValue1Size), this.ValueFormat1), + new( + this.Memory.Slice(offset + this.PairValue1Size, this.PairValue2Size), + this.ValueFormat2)); + } + } + + public IEnumerable ExtractAdvanceX() + { + if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && + (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) + { + yield break; + } + + var classes1 = this.ClassDefTable1.Enumerate() + .GroupBy(x => x.Class, x => x.GlyphId) + .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); + + var classes2 = this.ClassDefTable2.Enumerate() + .GroupBy(x => x.Class, x => x.GlyphId) + .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); + + foreach (var class1 in Enumerable.Range(0, this.Class1Count)) + { + if (!classes1.TryGetValue((ushort)class1, out var glyphs1)) + continue; + + foreach (var class2 in Enumerable.Range(0, this.Class2Count)) + { + if (!classes2.TryGetValue((ushort)class2, out var glyphs2)) + continue; + + (ValueRecord, ValueRecord) record; + try + { + record = this[class1, class2]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + var val = record.Item1.AdvanceX + record.Item2.PlacementX; + if (val == 0) + continue; + + foreach (var glyph1 in glyphs1) + { + foreach (var glyph2 in glyphs2) + { + yield return new(glyph1, glyph2, (short)val); + } + } + } + } + } + } + } + + public readonly struct ExtensionPositioningSubtableFormat1 + { + public readonly PointerSpan Memory; + + public ExtensionPositioningSubtableFormat1(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public LookupType ExtensionLookupType => this.Memory.ReadEnumBig(2); + + public int ExtensionOffset => this.Memory.ReadI32Big(4); + + public PointerSpan ExtensionData => this.Memory[this.ExtensionOffset..]; + } + } + + private readonly struct Head + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/head + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html + + public const uint MagicNumberValue = 0x5F0F3CF5; + public static readonly TagStruct DirectoryTableTag = new('h', 'e', 'a', 'd'); + + public readonly PointerSpan Memory; + + public Head(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Head(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum HeadFlags : ushort + { + BaselineForFontAtZeroY = 1 << 0, + LeftSideBearingAtZeroX = 1 << 1, + InstructionsDependOnPointSize = 1 << 2, + ForcePpemsInteger = 1 << 3, + InstructionsAlterAdvanceWidth = 1 << 4, + VerticalLayout = 1 << 5, + Reserved6 = 1 << 6, + RequiresLayoutForCorrectLinguisticRendering = 1 << 7, + IsAatFont = 1 << 8, + ContainsRtlGlyph = 1 << 9, + ContainsIndicStyleRearrangementEffects = 1 << 10, + Lossless = 1 << 11, + ProduceCompatibleMetrics = 1 << 12, + OptimizedForClearType = 1 << 13, + IsLastResortFont = 1 << 14, + Reserved15 = 1 << 15, + } + + [Flags] + public enum MacStyleFlags : ushort + { + Bold = 1 << 0, + Italic = 1 << 1, + Underline = 1 << 2, + Outline = 1 << 3, + Shadow = 1 << 4, + Condensed = 1 << 5, + Extended = 1 << 6, + } + + public Fixed Version => new(this.Memory); + + public Fixed FontRevision => new(this.Memory[4..]); + + public uint ChecksumAdjustment => this.Memory.ReadU32Big(8); + + public uint MagicNumber => this.Memory.ReadU32Big(12); + + public HeadFlags Flags => this.Memory.ReadEnumBig(16); + + public ushort UnitsPerEm => this.Memory.ReadU16Big(18); + + public ulong CreatedTimestamp => this.Memory.ReadU64Big(20); + + public ulong ModifiedTimestamp => this.Memory.ReadU64Big(28); + + public ushort MinX => this.Memory.ReadU16Big(36); + + public ushort MinY => this.Memory.ReadU16Big(38); + + public ushort MaxX => this.Memory.ReadU16Big(40); + + public ushort MaxY => this.Memory.ReadU16Big(42); + + public MacStyleFlags MacStyle => this.Memory.ReadEnumBig(44); + + public ushort LowestRecommendedPpem => this.Memory.ReadU16Big(46); + + public ushort FontDirectionHint => this.Memory.ReadU16Big(48); + + public ushort IndexToLocFormat => this.Memory.ReadU16Big(50); + + public ushort GlyphDataFormat => this.Memory.ReadU16Big(52); + } + + private readonly struct Kern + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/kern + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html + + public static readonly TagStruct DirectoryTableTag = new('k', 'e', 'r', 'n'); + + public readonly PointerSpan Memory; + + public Kern(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Kern(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public IEnumerable EnumerateHorizontalPairs() => this.Version switch + { + 0 => new Version0(this.Memory).EnumerateHorizontalPairs(), + 1 => new Version1(this.Memory).EnumerateHorizontalPairs(), + _ => Array.Empty(), + }; + + public readonly struct Format0 + { + public readonly PointerSpan Memory; + + public Format0(PointerSpan memory) => this.Memory = memory; + + public ushort PairCount => this.Memory.ReadU16Big(0); + + public ushort SearchRange => this.Memory.ReadU16Big(2); + + public ushort EntrySelector => this.Memory.ReadU16Big(4); + + public ushort RangeShift => this.Memory.ReadU16Big(6); + + public BigEndianPointerSpan Pairs => new( + this.Memory[8..].As(this.PairCount), + KerningPair.ReverseEndianness); + } + + public readonly struct Version0 + { + public readonly PointerSpan Memory; + + public Version0(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum CoverageFlags : byte + { + Horizontal = 1 << 0, + Minimum = 1 << 1, + CrossStream = 1 << 2, + Override = 1 << 3, + } + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort NumSubtables => this.Memory.ReadU16Big(2); + + public PointerSpan Data => this.Memory[4..]; + + public IEnumerable EnumerateSubtables() + { + var data = this.Data; + for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) + { + var st = new Subtable(data); + data = data[st.Length..]; + yield return st; + } + } + + public IEnumerable EnumerateHorizontalPairs() + { + var accumulator = new Dictionary<(ushort Left, ushort Right), short>(); + foreach (var subtable in this.EnumerateSubtables()) + { + var isOverride = (subtable.Flags & CoverageFlags.Override) != 0; + var isMinimum = (subtable.Flags & CoverageFlags.Minimum) != 0; + foreach (var t in subtable.EnumeratePairs()) + { + if (isOverride) + { + accumulator[(t.Left, t.Right)] = t.Value; + } + else if (isMinimum) + { + accumulator[(t.Left, t.Right)] = Math.Max( + accumulator.GetValueOrDefault((t.Left, t.Right), t.Value), + t.Value); + } + else + { + accumulator[(t.Left, t.Right)] = (short)( + accumulator.GetValueOrDefault( + (t.Left, t.Right)) + t.Value); + } + } + } + + return accumulator.Select( + x => new KerningPair { Left = x.Key.Left, Right = x.Key.Right, Value = x.Value }); + } + + public readonly struct Subtable + { + public readonly PointerSpan Memory; + + public Subtable(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public byte Format => this.Memory[4]; + + public CoverageFlags Flags => this.Memory.ReadEnumBig(5); + + public PointerSpan Data => this.Memory[6..]; + + public IEnumerable EnumeratePairs() => this.Format switch + { + 0 => new Format0(this.Data).Pairs, + _ => Array.Empty(), + }; + } + } + + public readonly struct Version1 + { + public readonly PointerSpan Memory; + + public Version1(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum CoverageFlags : byte + { + Vertical = 1 << 0, + CrossStream = 1 << 1, + Variation = 1 << 2, + } + + public Fixed Version => new(this.Memory); + + public int NumSubtables => this.Memory.ReadI16Big(4); + + public PointerSpan Data => this.Memory[8..]; + + public IEnumerable EnumerateSubtables() + { + var data = this.Data; + for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) + { + var st = new Subtable(data); + data = data[st.Length..]; + yield return st; + } + } + + public IEnumerable EnumerateHorizontalPairs() => this + .EnumerateSubtables() + .Where(x => x.Flags == 0) + .SelectMany(x => x.EnumeratePairs()); + + public readonly struct Subtable + { + public readonly PointerSpan Memory; + + public Subtable(PointerSpan memory) => this.Memory = memory; + + public int Length => this.Memory.ReadI32Big(0); + + public byte Format => this.Memory[4]; + + public CoverageFlags Flags => this.Memory.ReadEnumBig(5); + + public ushort TupleIndex => this.Memory.ReadU16Big(6); + + public PointerSpan Data => this.Memory[8..]; + + public IEnumerable EnumeratePairs() => this.Format switch + { + 0 => new Format0(this.Data).Pairs, + _ => Array.Empty(), + }; + } + } + } + + private readonly struct Name + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/name + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html + + public static readonly TagStruct DirectoryTableTag = new('n', 'a', 'm', 'e'); + + public readonly PointerSpan Memory; + + public Name(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Name(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort Count => this.Memory.ReadU16Big(2); + + public ushort StorageOffset => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan NameRecords => new( + this.Memory[6..].As(this.Count), + NameRecord.ReverseEndianness); + + public ushort LanguageCount => + this.Version == 0 ? (ushort)0 : this.Memory.ReadU16Big(6 + this.NameRecords.ByteCount); + + public BigEndianPointerSpan LanguageRecords => this.Version == 0 + ? default + : new( + this.Memory[ + (8 + this.NameRecords + .ByteCount)..] + .As( + this.LanguageCount), + LanguageRecord.ReverseEndianness); + + public PointerSpan Storage => this.Memory[this.StorageOffset..]; + + public string this[in NameRecord record] => + record.PlatformAndEncoding.Decode(this.Storage.Span.Slice(record.StringOffset, record.Length)); + + public string this[in LanguageRecord record] => + Encoding.ASCII.GetString(this.Storage.Span.Slice(record.LanguageTagOffset, record.Length)); + + public struct NameRecord + { + public PlatformAndEncoding PlatformAndEncoding; + public ushort LanguageId; + public NameId NameId; + public ushort Length; + public ushort StringOffset; + + public NameRecord(PointerSpan span) + { + this.PlatformAndEncoding = new(span); + var offset = Unsafe.SizeOf(); + span.ReadBig(ref offset, out this.LanguageId); + span.ReadBig(ref offset, out this.NameId); + span.ReadBig(ref offset, out this.Length); + span.ReadBig(ref offset, out this.StringOffset); + } + + public static NameRecord ReverseEndianness(NameRecord value) => new() + { + PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), + LanguageId = BinaryPrimitives.ReverseEndianness(value.LanguageId), + NameId = (NameId)BinaryPrimitives.ReverseEndianness((ushort)value.NameId), + Length = BinaryPrimitives.ReverseEndianness(value.Length), + StringOffset = BinaryPrimitives.ReverseEndianness(value.StringOffset), + }; + } + + public struct LanguageRecord + { + public ushort Length; + public ushort LanguageTagOffset; + + public LanguageRecord(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Length); + span.ReadBig(ref offset, out this.LanguageTagOffset); + } + + public static LanguageRecord ReverseEndianness(LanguageRecord value) => new() + { + Length = BinaryPrimitives.ReverseEndianness(value.Length), + LanguageTagOffset = BinaryPrimitives.ReverseEndianness(value.LanguageTagOffset), + }; + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs new file mode 100644 index 000000000..1d437d56d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs @@ -0,0 +1,135 @@ +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + /// + /// Checks whether the given will fail in , + /// and throws an appropriate exception if it is the case. + /// + /// The font config. + public static unsafe void CheckImGuiCompatibleOrThrow(in ImFontConfig fontConfig) + { + var ranges = fontConfig.GlyphRanges; + var sfnt = AsSfntFile(fontConfig); + var cmap = new Cmap(sfnt); + if (cmap.UnicodeTable is not { } unicodeTable) + throw new NotSupportedException("The font does not have a compatible Unicode character mapping table."); + if (unicodeTable.All(x => !ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(x.Key, ranges))) + throw new NotSupportedException("The font does not have any glyph that falls under the requested range."); + } + + /// + /// Enumerates through horizontal pair adjustments of a kern and gpos tables. + /// + /// The font config. + /// The enumerable of pair adjustments. Distance values need to be multiplied by font size in pixels. + public static IEnumerable<(char Left, char Right, float Distance)> ExtractHorizontalPairAdjustments( + ImFontConfig fontConfig) + { + float multiplier; + Dictionary glyphToCodepoints; + Gpos gpos = default; + Kern kern = default; + + try + { + var sfnt = AsSfntFile(fontConfig); + var head = new Head(sfnt); + multiplier = 3f / 4 / head.UnitsPerEm; + + if (new Cmap(sfnt).UnicodeTable is not { } table) + yield break; + + if (sfnt.ContainsKey(Kern.DirectoryTableTag)) + kern = new(sfnt); + else if (sfnt.ContainsKey(Gpos.DirectoryTableTag)) + gpos = new(sfnt); + else + yield break; + + glyphToCodepoints = table + .GroupBy(x => x.Value, x => x.Key) + .OrderBy(x => x.Key) + .ToDictionary( + x => x.Key, + x => x.Where(y => y <= ushort.MaxValue) + .Select(y => (char)y) + .ToArray()); + } + catch + { + // don't care; give up + yield break; + } + + if (kern.Memory.Count != 0) + { + foreach (var pair in kern.EnumerateHorizontalPairs()) + { + if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) + continue; + if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) + continue; + + foreach (var l in leftChars) + { + foreach (var r in rightChars) + yield return (l, r, pair.Value * multiplier); + } + } + } + else if (gpos.Memory.Count != 0) + { + foreach (var pair in gpos.ExtractAdvanceX()) + { + if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) + continue; + if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) + continue; + + foreach (var l in leftChars) + { + foreach (var r in rightChars) + yield return (l, r, pair.Value * multiplier); + } + } + } + } + + private static unsafe SfntFile AsSfntFile(in ImFontConfig fontConfig) + { + var memory = new PointerSpan((byte*)fontConfig.FontData, fontConfig.FontDataSize); + if (memory.Length < 4) + throw new NotSupportedException("File is too short to even have a magic."); + + var magic = memory.ReadU32Big(0); + if (BitConverter.IsLittleEndian) + magic = BinaryPrimitives.ReverseEndianness(magic); + + if (magic == SfntFile.FileTagTrueType1.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagType1.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagOpenTypeWithCff.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagOpenType1_0.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagTrueTypeApple.NativeValue) + return new(memory); + if (magic == TtcFile.FileTag.NativeValue) + return new TtcFile(memory)[fontConfig.FontNo]; + + throw new NotSupportedException($"The given file with the magic 0x{magic:X08} is not supported."); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs new file mode 100644 index 000000000..812608973 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -0,0 +1,291 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Managed version of , to avoid unnecessary heap allocation and use of unsafe blocks. +/// +public struct SafeFontConfig +{ + /// + /// The raw config. + /// + public ImFontConfig Raw; + + /// + /// Initializes a new instance of the struct. + /// + public SafeFontConfig() + { + this.OversampleH = 1; + this.OversampleV = 1; + this.PixelSnapH = true; + this.GlyphMaxAdvanceX = float.MaxValue; + this.RasterizerMultiply = 1f; + this.RasterizerGamma = 1.4f; + this.EllipsisChar = unchecked((char)-1); + this.Raw.FontDataOwnedByAtlas = 1; + } + + /// + /// Gets or sets the index of font within a TTF/OTF file. + /// + public int FontNo + { + get => this.Raw.FontNo; + set => this.Raw.FontNo = EnsureRange(value, 0, int.MaxValue); + } + + /// + /// Gets or sets the desired size of the new font, in pixels.
+ /// Effectively, this is the line height.
+ /// Value is tied with . + ///
+ public float SizePx + { + get => this.Raw.SizePixels; + set => this.Raw.SizePixels = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the desired size of the new font, in points.
+ /// Effectively, this is the line height.
+ /// Value is tied with . + ///
+ public float SizePt + { + get => (this.Raw.SizePixels * 3) / 4; + set => this.Raw.SizePixels = EnsureRange((value * 4) / 3, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the horizontal oversampling pixel count.
+ /// Rasterize at higher quality for sub-pixel positioning.
+ /// Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory.
+ /// Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + ///
+ public int OversampleH + { + get => this.Raw.OversampleH; + set => this.Raw.OversampleH = EnsureRange(value, 1, int.MaxValue); + } + + /// + /// Gets or sets the vertical oversampling pixel count.
+ /// Rasterize at higher quality for sub-pixel positioning.
+ /// This is not really useful as we don't use sub-pixel positions on the Y axis. + ///
+ public int OversampleV + { + get => this.Raw.OversampleV; + set => this.Raw.OversampleV = EnsureRange(value, 1, int.MaxValue); + } + + /// + /// Gets or sets a value indicating whether to align every glyph to pixel boundary.
+ /// Useful e.g. if you are merging a non-pixel aligned font with the default font.
+ /// If enabled, you can set and to 1. + ///
+ public bool PixelSnapH + { + get => this.Raw.PixelSnapH != 0; + set => this.Raw.PixelSnapH = value ? (byte)1 : (byte)0; + } + + /// + /// Gets or sets the extra spacing (in pixels) between glyphs.
+ /// Only X axis is supported for now.
+ /// Effectively, it is the letter spacing. + ///
+ public Vector2 GlyphExtraSpacing + { + get => this.Raw.GlyphExtraSpacing; + set => this.Raw.GlyphExtraSpacing = new( + EnsureRange(value.X, float.MinValue, float.MaxValue), + EnsureRange(value.Y, float.MinValue, float.MaxValue)); + } + + /// + /// Gets or sets the offset all glyphs from this font input.
+ /// Use this to offset fonts vertically when merging multiple fonts. + ///
+ public Vector2 GlyphOffset + { + get => this.Raw.GlyphOffset; + set => this.Raw.GlyphOffset = new( + EnsureRange(value.X, float.MinValue, float.MaxValue), + EnsureRange(value.Y, float.MinValue, float.MaxValue)); + } + + /// + /// Gets or sets the glyph ranges, which is a user-provided list of Unicode range. + /// Each range has 2 values, and values are inclusive.
+ /// The list must be zero-terminated.
+ /// If empty or null, then all the glyphs from the font that is in the range of UCS-2 will be added. + ///
+ public ushort[]? GlyphRanges { get; set; } + + /// + /// Gets or sets the minimum AdvanceX for glyphs.
+ /// Set only to align font icons.
+ /// Set both / to enforce mono-space font. + ///
+ public float GlyphMinAdvanceX + { + get => this.Raw.GlyphMinAdvanceX; + set => this.Raw.GlyphMinAdvanceX = + float.IsFinite(value) + ? value + : throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); + } + + /// + /// Gets or sets the maximum AdvanceX for glyphs. + /// + public float GlyphMaxAdvanceX + { + get => this.Raw.GlyphMaxAdvanceX; + set => this.Raw.GlyphMaxAdvanceX = + float.IsFinite(value) + ? value + : throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); + } + + /// + /// Gets or sets a value that either brightens (>1.0f) or darkens (<1.0f) the font output.
+ /// Brightening small fonts may be a good workaround to make them more readable. + ///
+ public float RasterizerMultiply + { + get => this.Raw.RasterizerMultiply; + set => this.Raw.RasterizerMultiply = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the gamma value for fonts. + /// + public float RasterizerGamma + { + get => this.Raw.RasterizerGamma; + set => this.Raw.RasterizerGamma = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets a value explicitly specifying unicode codepoint of the ellipsis character.
+ /// When fonts are being merged first specified ellipsis will be used. + ///
+ public char EllipsisChar + { + get => (char)this.Raw.EllipsisChar; + set => this.Raw.EllipsisChar = value; + } + + /// + /// Gets or sets the desired name of the new font. Names longer than 40 bytes will be partially lost. + /// + public unsafe string Name + { + get + { + fixed (void* pName = this.Raw.Name) + { + var span = new ReadOnlySpan(pName, 40); + var firstNull = span.IndexOf((byte)0); + if (firstNull != -1) + span = span[..firstNull]; + return Encoding.UTF8.GetString(span); + } + } + + set + { + fixed (void* pName = this.Raw.Name) + { + var span = new Span(pName, 40); + Encoding.UTF8.GetBytes(value, span); + } + } + } + + /// + /// Gets or sets the desired font to merge with, if set. + /// + public unsafe ImFontPtr MergeFont + { + get => this.Raw.DstFont is not null ? this.Raw.DstFont : default; + set + { + this.Raw.MergeMode = value.NativePtr is null ? (byte)0 : (byte)1; + this.Raw.DstFont = value.NativePtr is null ? default : value.NativePtr; + } + } + + /// + /// Throws with appropriate messages, + /// if this has invalid values. + /// + public readonly void ThrowOnInvalidValues() + { + if (!(this.Raw.FontNo >= 0)) + throw new ArgumentException($"{nameof(this.FontNo)} must not be a negative number."); + + if (!(this.Raw.SizePixels > 0)) + throw new ArgumentException($"{nameof(this.SizePx)} must be a positive number."); + + if (!(this.Raw.OversampleH >= 1)) + throw new ArgumentException($"{nameof(this.OversampleH)} must be a negative number."); + + if (!(this.Raw.OversampleV >= 1)) + throw new ArgumentException($"{nameof(this.OversampleV)} must be a negative number."); + + if (!float.IsFinite(this.Raw.GlyphMinAdvanceX)) + throw new ArgumentException($"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); + + if (!float.IsFinite(this.Raw.GlyphMaxAdvanceX)) + throw new ArgumentException($"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); + + if (!(this.Raw.RasterizerMultiply > 0)) + throw new ArgumentException($"{nameof(this.RasterizerMultiply)} must be a positive number."); + + if (!(this.Raw.RasterizerGamma > 0)) + throw new ArgumentException($"{nameof(this.RasterizerGamma)} must be a positive number."); + + if (this.GlyphRanges is { Length: > 0 } ranges) + { + if (ranges[0] == 0) + { + throw new ArgumentException( + "Font ranges cannot start with 0.", + nameof(this.GlyphRanges)); + } + + if (ranges[(ranges.Length - 1) & ~1] != 0) + { + throw new ArgumentException( + "Font ranges must terminate with a zero at even indices.", + nameof(this.GlyphRanges)); + } + } + } + + private static T EnsureRange(T value, T min, T max, [CallerMemberName] string callerName = "") + where T : INumber + { + if (value < min) + throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be less than {min}."); + if (value > max) + throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be more than {max}."); + + return value; + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index dd2e5bad3..f7beb22fa 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -12,6 +11,8 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -30,11 +31,14 @@ public sealed class UiBuilder : IDisposable private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); - private readonly GameFontManager gameFontManager = Service.Get(); + private readonly Framework framework = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -45,14 +49,32 @@ public sealed class UiBuilder : IDisposable /// The plugin namespace. internal UiBuilder(string namespaceName) { - this.stopwatch = new Stopwatch(); - this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); - this.namespaceName = namespaceName; + try + { + this.stopwatch = new Stopwatch(); + this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); + this.namespaceName = namespaceName; - this.interfaceManager.Draw += this.OnDraw; - this.interfaceManager.BuildFonts += this.OnBuildFonts; - this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; - this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.interfaceManager.Draw += this.OnDraw; + this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw); + + this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); + + this.privateAtlas = + this.scopedFinalizer + .Add( + Service + .Get() + .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); + this.privateAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; + this.privateAtlas.RebuildRecommend += this.RebuildFonts; + } + catch + { + this.scopedFinalizer.Dispose(); + throw; + } } /// @@ -80,19 +102,19 @@ public sealed class UiBuilder : IDisposable /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler.
- /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + /// pointers inside this handler. ///
- public event Action BuildFonts; + [Obsolete($"Use {nameof(NewDelegateFontHandle)} instead.", false)] + public event Action? BuildFonts; /// /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler.
- /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + /// pointers inside this handler. ///
- public event Action AfterBuildFonts; + [Obsolete($"Use {nameof(NewDelegateFontHandle)} instead.", false)] + public event Action? AfterBuildFonts; /// /// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown. @@ -107,18 +129,57 @@ public sealed class UiBuilder : IDisposable public event Action HideUi; /// - /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. + /// Gets the default Dalamud font size in points. /// + public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt; + + /// + /// Gets the default Dalamud font size in pixels. + /// + public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx; + + /// + /// Gets the default Dalamud font - supporting all game languages and icons.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); + /// + /// public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; /// - /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
+ /// Accessing this static property outside of is dangerous and not supported. ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// + /// public static ImFontPtr IconFont => InterfaceManager.IconFont; /// - /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
+ /// Accessing this static property outside of is dangerous and not supported. ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddDalamudAssetFont( + /// DalamudAsset.InconsolataRegular, + /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// + /// public static ImFontPtr MonoFont => InterfaceManager.MonoFont; /// @@ -319,7 +380,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) .Unwrap(); } else @@ -341,7 +402,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) .Unwrap(); } else @@ -357,19 +418,74 @@ public sealed class UiBuilder : IDisposable /// /// Font to get. /// Handle to the game font which may or may not be available for use yet. - public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style); + [Obsolete($"Use {nameof(NewGameFontHandle)} instead.", false)] + public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( + (IFontHandle.IInternal)this.NewGameFontHandle(style), + Service.Get()); + + /// + public IFontHandle NewGameFontHandle(GameFontStyle style) => this.privateAtlas.NewGameFontHandle(style); + + /// + /// + /// On initialization: + /// + /// this.fontHandle = uiBuilder.NewDelegateFontHandle(e => e.OnPreBuild(tk => { + /// var config = new SafeFontConfig { SizePx = 16 }; + /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); + /// tk.AddGameSymbol(config); + /// tk.AddExtraGlyphsForDalamudLanguage(config); + /// // optional: tk.Font = config.MergeFont; + /// })); + /// + /// + /// On use: + /// + /// using (this.fontHandle.Push()) + /// ImGui.TextUnformatted("Example"); + /// + /// + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => + this.privateAtlas.NewDelegateFontHandle(buildStepDelegate); /// /// Call this to queue a rebuild of the font atlas.
- /// This will invoke any handlers and ensure that any loaded fonts are - /// ready to be used on the next UI frame. + /// This will invoke any and handlers and ensure that any + /// loaded fonts are ready to be used on the next UI frame. ///
public void RebuildFonts() { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); - this.interfaceManager.RebuildFonts(); + if (this.AfterBuildFonts is null && this.BuildFonts is null) + this.privateAtlas.BuildFontsAsync(); + else + this.privateAtlas.BuildFontsOnNextFrame(); } + /// + /// Creates an isolated . + /// + /// Specify when and how to rebuild this atlas. + /// Whether the fonts in the atlas is global scaled. + /// Name for debugging purposes. + /// A new instance of . + /// + /// Use this to create extra font atlases, if you want to create and dispose fonts without having to rebuild all + /// other fonts together.
+ /// If is not , + /// the font rebuilding functions must be called manually. + ///
+ public IFontAtlas CreateFontAtlas( + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled = true, + string? debugName = null) => + this.scopedFinalizer.Add(Service + .Get() + .CreateFontAtlas( + this.namespaceName + ":" + (debugName ?? "custom"), + autoRebuildMode, + isGlobalScaled)); + /// /// Add a notification to the notification queue. /// @@ -392,12 +508,7 @@ public sealed class UiBuilder : IDisposable /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() - { - this.interfaceManager.Draw -= this.OnDraw; - this.interfaceManager.BuildFonts -= this.OnBuildFonts; - this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; - } + void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); /// /// Open the registered configuration UI, if it exists. @@ -463,8 +574,12 @@ public sealed class UiBuilder : IDisposable this.ShowUi?.InvokeSafely(); } - if (!this.interfaceManager.FontsReady) + // just in case, if something goes wrong, prevent drawing; otherwise it probably will crash. + if (!this.privateAtlas.BuildTask.IsCompletedSuccessfully + && (this.BuildFonts is not null || this.AfterBuildFonts is not null)) + { return; + } ImGui.PushID(this.namespaceName); if (DoStats) @@ -526,14 +641,28 @@ public sealed class UiBuilder : IDisposable this.hitchDetector.Stop(); } - private void OnBuildFonts() + private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e) { - this.BuildFonts?.InvokeSafely(); - } + if (e.IsAsyncBuildOperation) + return; - private void OnAfterBuildFonts() - { - this.AfterBuildFonts?.InvokeSafely(); + e.OnPreBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + this.BuildFonts?.InvokeSafely(); + ImGui.GetIO().NativePtr->Fonts = prev; + }); + + e.OnPostBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + this.AfterBuildFonts?.InvokeSafely(); + ImGui.GetIO().NativePtr->Fonts = prev; + }); } private void OnResizeBuffers() diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 85f81b203..80329f558 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,10 +1,15 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Numerics; +using System.Reactive.Disposables; using System.Runtime.InteropServices; +using System.Text.Unicode; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using ImGuiScene; @@ -31,7 +36,7 @@ public static class ImGuiHelpers /// This does not necessarily mean you can call drawing functions. /// public static unsafe bool IsImGuiInitialized => - ImGui.GetCurrentContext() is not 0 && ImGui.GetIO().NativePtr is not null; + ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null; /// /// Gets the global Dalamud scale; even available before drawing is ready.
@@ -342,25 +347,18 @@ public static class ImGuiHelpers } if (changed && rebuildLookupTable) - target.BuildLookupTableNonstandard(); - } + { + // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. + // FallbackGlyph is resolved after resolving ' '. + // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, + // making FindGlyph return nullptr. + // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, + // making ImGui attempt to treat whatever was there as a ' '. + // This may cause random glyphs to be sized randomly, if not an access violation exception. + target.NativePtr->FallbackGlyph = null; - /// - /// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions. - /// - /// The font. - public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font) - { - // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. - // FallbackGlyph is resolved after resolving ' '. - // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, - // making FindGlyph return nullptr. - // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, - // making ImGui attempt to treat whatever was there as a ' '. - // This may cause random glyphs to be sized randomly, if not an access violation exception. - font.NativePtr->FallbackGlyph = null; - - font.BuildLookupTable(); + target.BuildLookupTable(); + } } /// @@ -406,6 +404,129 @@ public static class ImGuiHelpers public static void CenterCursorFor(float itemWidth) => ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); + /// + /// Allocates memory on the heap using
+ /// Memory must be freed using . + ///
+ /// Note that null is a valid return value when is 0. + ///
+ /// The length of allocated memory. + /// The allocated memory. + /// If returns null. + public static unsafe void* AllocateMemory(int length) + { + switch (length) + { + case 0: + return null; + case < 0: + throw new ArgumentOutOfRangeException( + nameof(length), + length, + $"{nameof(length)} cannot be a negative number."); + default: + var memory = ImGuiNative.igMemAlloc((uint)length); + if (memory is null) + { + throw new OutOfMemoryException( + $"Failed to allocate {length} bytes using {nameof(ImGuiNative.igMemAlloc)}"); + } + + return memory; + } + } + + /// + /// Mark 4K page as used, after adding a codepoint to a font. + /// + /// The font. + /// The codepoint. + public static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) + { + // Mark 4K page as used + var pageIndex = unchecked((ushort)(codepoint / 4096)); + font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); + } + + /// + /// Creates a new instance of with a natively backed memory. + /// + /// The created instance. + /// Disposable you can call. + public static unsafe IDisposable NewFontAtlasPtrScoped(out ImFontAtlasPtr font) + { + font = new(ImGuiNative.ImFontAtlas_ImFontAtlas()); + var ptr = font.NativePtr; + return Disposable.Create(() => + { + if (ptr != null) + ImGuiNative.ImFontAtlas_destroy(ptr); + ptr = null; + }); + } + + /// + /// Creates a new instance of with a natively backed memory. + /// + /// The created instance. + /// Disposable you can call. + public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder) + { + builder = new(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder()); + var ptr = builder.NativePtr; + return Disposable.Create(() => + { + if (ptr != null) + ImGuiNative.ImFontGlyphRangesBuilder_destroy(ptr); + ptr = null; + }); + } + + /// + /// Builds ImGui Glyph Ranges for use with . + /// + /// The builder. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// When disposed, the resource allocated for the range will be freed. + public static unsafe ushort[] BuildRangesToArray( + this ImFontGlyphRangesBuilderPtr builder, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + if (addFallbackCodepoints) + builder.AddText(FontAtlasFactory.FallbackCodepoints); + if (addEllipsisCodepoints) + { + builder.AddText(FontAtlasFactory.EllipsisCodepoints); + builder.AddChar('.'); + } + + builder.BuildRanges(out var vec); + return new ReadOnlySpan((void*)vec.Data, vec.Size).ToArray(); + } + + /// + public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges) + => CreateImGuiRangesFrom((IEnumerable)ranges); + + /// + /// Creates glyph ranges from .
+ /// Use values from . + ///
+ /// The unicode ranges. + /// The range array that can be used for . + public static ushort[] CreateImGuiRangesFrom(IEnumerable ranges) => + ranges + .Where(x => x.FirstCodePoint <= ushort.MaxValue) + .SelectMany( + x => new[] + { + (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), + (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), + }) + .ToArray(); + /// /// Determines whether is empty. /// @@ -414,7 +535,7 @@ public static class ImGuiHelpers public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; /// - /// Determines whether is not null and loaded. + /// Determines whether is empty. /// /// The pointer. /// Whether it is empty. @@ -447,6 +568,98 @@ public static class ImGuiHelpers return -1; } + /// + /// If is default, then returns . + /// + /// The self. + /// The other. + /// if it is not default; otherwise, . + public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => + self.NativePtr is null ? other : self; + + /// + /// Attempts to validate that is valid. + /// + /// The font pointer. + /// The exception, if any occurred during validation. + internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr) + { + try + { + var font = fontPtr.NativePtr; + if (font is null) + throw new NullReferenceException("The font is null."); + + _ = Marshal.ReadIntPtr((nint)font); + if (font->IndexedHotData.Data != 0) + _ = Marshal.ReadIntPtr(font->IndexedHotData.Data); + if (font->FrequentKerningPairs.Data != 0) + _ = Marshal.ReadIntPtr(font->FrequentKerningPairs.Data); + if (font->IndexLookup.Data != 0) + _ = Marshal.ReadIntPtr(font->IndexLookup.Data); + if (font->Glyphs.Data != 0) + _ = Marshal.ReadIntPtr(font->Glyphs.Data); + if (font->KerningPairs.Data != 0) + _ = Marshal.ReadIntPtr(font->KerningPairs.Data); + if (font->ConfigDataCount == 0 && font->ConfigData is not null) + throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?"); + if (font->ConfigDataCount != 0 && font->ConfigData is null) + throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?"); + if (font->ConfigData is not null) + _ = Marshal.ReadIntPtr((nint)font->ConfigData); + if (font->FallbackGlyph is not null + && ((nint)font->FallbackGlyph < font->Glyphs.Data || (nint)font->FallbackGlyph >= font->Glyphs.Data)) + throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); + if (font->FallbackHotData is not null + && ((nint)font->FallbackHotData < font->IndexedHotData.Data + || (nint)font->FallbackHotData >= font->IndexedHotData.Data)) + throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); + if (font->ContainerAtlas is not null) + _ = Marshal.ReadIntPtr((nint)font->ContainerAtlas); + } + catch (Exception e) + { + return e; + } + + return null; + } + + /// + /// Updates the fallback char of . + /// + /// The font. + /// The fallback character. + internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c) + { + font.FallbackChar = c; + font.NativePtr->FallbackHotData = + (ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar); + } + + /// + /// Determines if the supplied codepoint is inside the given range, + /// in format of . + /// + /// The codepoint. + /// The ranges. + /// Whether it is the case. + internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr) + { + if (codepoint is <= 0 or >= ushort.MaxValue) + return false; + + while (*rangePtr != 0) + { + var from = *rangePtr++; + var to = *rangePtr++; + if (from <= codepoint && codepoint <= to) + return true; + } + + return false; + } + /// /// Get data needed for each new frame. /// From f8e6df1172e2b4eb87ee094aa94e219fa0f1ac99 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 29 Nov 2023 14:12:36 +0900 Subject: [PATCH 07/71] Add font build status display to Settings window --- .../Interface/Internal/InterfaceManager.cs | 9 ++++-- .../Windows/Settings/Tabs/SettingsTabLook.cs | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 5a6a2cbdb..e21a22fa2 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -4,9 +4,7 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; -using System.Text.Unicode; -using System.Threading; +using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -236,6 +234,11 @@ internal class InterfaceManager : IDisposable, IServiceType } } + /// + /// Gets the font build task. + /// + public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; + /// /// Dispose of managed and unmanaged resources. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 02e8ce789..ec140890f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; +using System.Text; using CheapLoc; using Dalamud.Configuration.Internal; @@ -145,6 +146,7 @@ public class SettingsTabLook : SettingsTab public override void Draw() { var interfaceManager = Service.Get(); + var fontBuildTask = interfaceManager.FontBuildTask; ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); @@ -164,6 +166,19 @@ public class SettingsTabLook : SettingsTab } } + if (!fontBuildTask.IsCompleted) + { + ImGui.SameLine(); + var buildingFonts = Loc.Localize("DalamudSettingsFontBuildInProgressWithEndingThreeDots", "Building fonts..."); + unsafe + { + var len = Encoding.UTF8.GetByteCount(buildingFonts); + var p = stackalloc byte[len]; + Encoding.UTF8.GetBytes(buildingFonts, new(p, len)); + ImGuiNative.igTextUnformatted(p, (p + len + ((Environment.TickCount / 200) % 3)) - 2); + } + } + var globalUiScaleInPt = 12f * this.globalUiScale; if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp)) { @@ -174,6 +189,19 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale text in all XIVLauncher UI elements - this is useful for 4K displays.")); + if (fontBuildTask.IsFaulted || fontBuildTask.IsCanceled) + { + ImGui.TextColored( + ImGuiColors.DalamudRed, + Loc.Localize("DalamudSettingsFontBuildFaulted", "Failed to load fonts as requested.")); + if (fontBuildTask.Exception is not null + && ImGui.CollapsingHeader("##DalamudSetingsFontBuildFaultReason")) + { + foreach (var e in fontBuildTask.Exception.InnerExceptions) + ImGui.TextUnformatted(e.ToString()); + } + } + ImGuiHelpers.ScaledDummy(5); ImGui.AlignTextToFramePadding(); @@ -208,6 +236,7 @@ public class SettingsTabLook : SettingsTab public override void Save() { Service.Get().GlobalUiScale = this.globalUiScale; + Service.Get().FontGammaLevel = this.fontGamma; base.Save(); } From 701d006db831270bbb8ff85f3652e06886b84d1d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 29 Nov 2023 14:13:11 +0900 Subject: [PATCH 08/71] Fix AddDalamudDefaultFont on not using lodestone symbol fonts file --- .../IFontAtlasBuildToolkitPreBuild.cs | 7 ++- .../Internals/DelegateFontHandle.cs | 5 +- .../FontAtlasFactory.BuildToolkit.cs | 5 +- .../Internals/GamePrebakedFontHandle.cs | 49 +++++++++++++------ 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index e8f11aec3..dbe8626e9 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -120,7 +120,9 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds the default font known to the current font atlas.
///
- /// Default font includes and . + /// Includes and .
+ /// As this involves adding multiple fonts, calling this function will set + /// as the return value of this function, if it was empty before. ///
/// Font size in pixels. /// The glyph ranges. Use .ToGlyphRange to build. @@ -132,7 +134,8 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit ///
/// Note: if game symbols font file is requested but is unavailable, /// then it will take the glyphs from game's built-in fonts, and everything in - /// will be ignored but and . + /// will be ignored but , , + /// and . ///
/// The font type. /// The font config. diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index bc48ddcc1..142bd73da 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -211,7 +211,8 @@ internal class DelegateFontHandle : IFontHandle.IInternal { Log.Warning( "[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " + - "did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", + "Using the most recently added font. " + + "Did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", this.Manager.Name, fontsVector.Length - fontCountPrevious, nameof(FontAtlasBuildStepDelegate), @@ -262,7 +263,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal { var distinct = fontsVector - .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates + .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates .Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them .ToArray(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 9ebf20fc7..8e115c126 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -291,6 +291,8 @@ internal sealed partial class FontAtlasFactory var font = this.AddDalamudAssetFont(DalamudAsset.NotoSansJpMedium, fontConfig); this.AddExtraGlyphsForDalamudLanguage(fontConfig with { MergeFont = font }); this.AddGameSymbol(fontConfig with { MergeFont = font }); + if (this.Font.IsNull()) + this.Font = font; return font; } @@ -316,7 +318,8 @@ internal sealed partial class FontAtlasFactory return this.gameFontHandleSubstance.AttachGameSymbols( this, fontConfig.MergeFont, - fontConfig.SizePx); + fontConfig.SizePx, + fontConfig.GlyphRanges); default: return this.factory.AddFont( diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 012613a38..37266f39b 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -214,9 +214,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal // Owned by this class, but ImFontPtr values still do not belong to this. private readonly Dictionary fonts = new(); private readonly Dictionary buildExceptions = new(); - - private readonly Dictionary fontsSymbolsOnly = new(); - private readonly Dictionary> symbolsCopyTargets = new(); + private readonly Dictionary> fontCopyTargets = new(); private readonly HashSet templatedFonts = new(); private readonly Dictionary> lateBuildRanges = new(); @@ -250,23 +248,24 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// The toolkitPostBuild. /// The font to attach to. /// The font size in pixels. + /// The intended glyph ranges. /// if it is not empty; otherwise a new font. - public ImFontPtr AttachGameSymbols(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, ImFontPtr font, float sizePx) + public ImFontPtr AttachGameSymbols( + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + ImFontPtr font, + float sizePx, + ushort[]? glyphRanges) { var style = new GameFontStyle(GameFontFamily.Axis, sizePx); - if (!this.fontsSymbolsOnly.TryGetValue(style, out var symbolFont)) - { - symbolFont = this.CreateFontPrivate(style, toolkitPreBuild, ' ', '\uFFFE', true); - this.fontsSymbolsOnly.Add(style, symbolFont); - } + var referenceFont = this.GetOrCreateFont(style, toolkitPreBuild); if (font.IsNull()) font = this.CreateTemplateFont(style, toolkitPreBuild); - if (!this.symbolsCopyTargets.TryGetValue(symbolFont, out var set)) - this.symbolsCopyTargets[symbolFont] = set = new(); + if (!this.fontCopyTargets.TryGetValue(referenceFont, out var copyTargets)) + this.fontCopyTargets[referenceFont] = copyTargets = new(); - set.Add(font); + copyTargets.Add((font, glyphRanges)); return font; } @@ -342,7 +341,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal for (var i = 0; i < pixels8Array.Length; i++) toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out heights[i]); - foreach (var (style, font) in this.fonts.Concat(this.fontsSymbolsOnly)) + foreach (var (style, font) in this.fonts) { try { @@ -585,10 +584,30 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } - foreach (var (source, targets) in this.symbolsCopyTargets) + foreach (var (source, targets) in this.fontCopyTargets) { foreach (var target in targets) - ImGuiHelpers.CopyGlyphsAcrossFonts(source, target, true, true, SeIconCharMin, SeIconCharMax); + { + if (target.Ranges is null) + { + ImGuiHelpers.CopyGlyphsAcrossFonts(source, target.Font, missingOnly: true); + } + else + { + for (var i = 0; i < target.Ranges.Length; i += 2) + { + if (target.Ranges[i] == 0) + break; + ImGuiHelpers.CopyGlyphsAcrossFonts( + source, + target.Font, + true, + true, + target.Ranges[i], + target.Ranges[i + 1]); + } + } + } } } From e86c5458a20582d6dbad3e15de89825c4bdddaf9 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 21:45:19 +0900 Subject: [PATCH 09/71] Remove font gamma configuration --- .../Internal/DalamudConfiguration.cs | 7 +-- .../Interface/Internal/InterfaceManager.cs | 10 ----- .../Windows/Settings/SettingsWindow.cs | 6 +-- .../Windows/Settings/Tabs/SettingsTabLook.cs | 23 ---------- .../FontAtlasFactory.BuildToolkit.cs | 3 +- .../Internals/FontAtlasFactory.cs | 44 +++++-------------- .../Internals/GamePrebakedFontHandle.cs | 16 ------- 7 files changed, 15 insertions(+), 94 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 76c8f3603..66c2745c5 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -148,12 +148,9 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable public bool UseAxisFontsFromGame { get; set; } = false; /// - /// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness. - /// - /// Before gamma is applied... - /// * ...TTF fonts loaded with stb or FreeType are in linear space. - /// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4. + /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use. /// + [Obsolete("It happens that nobody touched this setting", true)] public float FontGammaLevel { get; set; } = 1.4f; /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index e21a22fa2..46d37fe90 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -199,16 +199,6 @@ internal class InterfaceManager : IDisposable, IServiceType /// public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; - /// - /// Gets or sets the overrided font gamma value, instead of using the value from configuration. - /// - public float? FontGammaOverride { get; set; } = null; - - /// - /// Gets the font gamma value to use. - /// - public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGammaLevel)); - /// /// Gets a value indicating the native handle of the game main window. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 414eabd22..20ffc781c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -66,13 +66,9 @@ internal class SettingsWindow : Window var configuration = Service.Get(); var interfaceManager = Service.Get(); - var rebuildFont = - ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale || - interfaceManager.FontGamma != configuration.FontGammaLevel || - interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - interfaceManager.FontGammaOverride = null; interfaceManager.UseAxisOverride = null; if (rebuildFont) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index ec140890f..35f307655 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -29,7 +29,6 @@ public class SettingsTabLook : SettingsTab }; private float globalUiScale; - private float fontGamma; public override SettingsEntry[] Entries { get; } = { @@ -202,33 +201,12 @@ public class SettingsTabLook : SettingsTab } } - ImGuiHelpers.ScaledDummy(5); - - ImGui.AlignTextToFramePadding(); - ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); - ImGui.SameLine(); - if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) - { - this.fontGamma = 1.4f; - interfaceManager.FontGammaOverride = this.fontGamma; - interfaceManager.RebuildFonts(); - } - - if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp)) - { - interfaceManager.FontGammaOverride = this.fontGamma; - interfaceManager.RebuildFonts(); - } - - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFontGammaHint", "Changes the thickness of text.")); - base.Draw(); } public override void Load() { this.globalUiScale = Service.Get().GlobalUiScale; - this.fontGamma = Service.Get().FontGammaLevel; base.Load(); } @@ -236,7 +214,6 @@ public class SettingsTabLook : SettingsTab public override void Save() { Service.Get().GlobalUiScale = this.globalUiScale; - Service.Get().FontGammaLevel = this.fontGamma; base.Save(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 8e115c126..4403d6400 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -381,7 +381,6 @@ internal sealed partial class FontAtlasFactory public unsafe void PreBuild() { - var gamma = this.factory.InterfaceManager.FontGamma; var configData = this.data.ConfigData; foreach (ref var config in configData.DataSpan) { @@ -400,7 +399,7 @@ internal sealed partial class FontAtlasFactory config.GlyphOffset *= this.Scale; - config.RasterizerGamma *= gamma; + config.RasterizerGamma *= 1.4f; } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 7a1926a9d..fc199ef5a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -39,8 +39,6 @@ internal sealed partial class FontAtlasFactory private readonly Task defaultGlyphRanges; private readonly DalamudAssetManager dalamudAssetManager; - private float lastBuildGamma = -1f; - [ServiceManager.ServiceConstructor] private FontAtlasFactory( DataManager dataManager, @@ -210,18 +208,10 @@ internal sealed partial class FontAtlasFactory { lock (this.prebakedTextureWraps[texPathFormat]) { - var gamma = this.InterfaceManager.FontGamma; var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]); - if (Math.Abs(this.lastBuildGamma - gamma) > 0.0001f) - { - this.lastBuildGamma = gamma; - wraps.AggregateToDisposable().Dispose(); - wraps.AsSpan().Clear(); - } - var fileIndex = textureIndex / 4; var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4]; - wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex, gamma); + wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex); return CloneTextureWrap(wraps[textureIndex]); } } @@ -232,13 +222,9 @@ internal sealed partial class FontAtlasFactory Span target, ReadOnlySpan source, int channelIndex, - bool targetIsB4G4R4A4, - float gamma) + bool targetIsB4G4R4A4) { var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); - var gammaTable = stackalloc byte[256]; - for (var i = 0; i < 256; i++) - gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 255f, 0, 1), 1.4f / gamma) * 255); fixed (byte* sourcePtrImmutable = source) { @@ -250,7 +236,7 @@ internal sealed partial class FontAtlasFactory var wptr = (ushort*)targetPtr; while (numPixels-- > 0) { - *wptr = (ushort)((gammaTable[*rptr] << 8) | 0x0FFF); + *wptr = (ushort)((*rptr << 8) | 0x0FFF); wptr++; rptr += 4; } @@ -260,7 +246,7 @@ internal sealed partial class FontAtlasFactory var wptr = (uint*)targetPtr; while (numPixels-- > 0) { - *wptr = (uint)((gammaTable[*rptr] << 24) | 0x00FFFFFF); + *wptr = (uint)((*rptr << 24) | 0x00FFFFFF); wptr++; rptr += 4; } @@ -292,41 +278,33 @@ internal sealed partial class FontAtlasFactory Span target, ReadOnlySpan source, int channelIndex, - bool targetIsB4G4R4A4, - float gamma) + bool targetIsB4G4R4A4) { var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); fixed (byte* sourcePtrImmutable = source) { var rptr = sourcePtrImmutable + (channelIndex / 2); var rshift = (channelIndex & 1) == 0 ? 0 : 4; - var gammaTable = stackalloc byte[256]; fixed (void* targetPtr = target) { if (targetIsB4G4R4A4) { - for (var i = 0; i < 16; i++) - gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 15f, 0, 1), 1.4f / gamma) * 15); - var wptr = (ushort*)targetPtr; while (numPixels-- > 0) { - *wptr = (ushort)((gammaTable[(*rptr >> rshift) & 0xF] << 12) | 0x0FFF); + *wptr = (ushort)(((*rptr >> rshift) << 12) | 0x0FFF); wptr++; rptr += 2; } } else { - for (var i = 0; i < 256; i++) - gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 255f, 0, 1), 1.4f / gamma) * 255); - var wptr = (uint*)targetPtr; while (numPixels-- > 0) { var v = (*rptr >> rshift) & 0xF; v |= v << 4; - *wptr = (uint)((gammaTable[v] << 24) | 0x00FFFFFF); + *wptr = (uint)((v << 24) | 0x00FFFFFF); wptr++; rptr += 4; } @@ -335,7 +313,7 @@ internal sealed partial class FontAtlasFactory } } - private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex, float gamma) + private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) { var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); var numPixels = texFile.Header.Width * texFile.Header.Height; @@ -351,15 +329,15 @@ internal sealed partial class FontAtlasFactory { case TexFile.TextureFormat.B4G4R4A4: // Game ships with this format. - ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4, gamma); + ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); break; case TexFile.TextureFormat.B8G8R8A8: // In case of modded font textures. - ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4, gamma); + ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); break; default: // Unlikely. - ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4, gamma); + ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4); break; } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 37266f39b..c40302f6c 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -334,7 +334,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal ArrayPool.Shared.Return(x); }); - var fontGamma = this.interfaceManager.FontGamma; var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; var heights = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; @@ -447,21 +446,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } } - - if (Math.Abs(fontGamma - 1.4f) >= 0.001) - { - // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) - var xTo = rc->X + rc->Width; - var yTo = rc->Y + rc->Height; - for (int y = rc->Y; y < yTo; y++) - { - for (int x = rc->X; x < xTo; x++) - { - var i = (y * width) + x; - pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); - } - } - } } } else if (this.lateBuildRanges.TryGetValue(font, out var buildRanges)) From aa3b991932d48b999c84bd6c2cd9a8f3b2ae69aa Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 21:48:50 +0900 Subject: [PATCH 10/71] Minor fix --- Dalamud/Interface/Internal/Windows/ChangelogWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 9b0416583..ae59db36a 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -82,7 +82,7 @@ internal sealed class ChangelogWindow : Window, IDisposable // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - _ = this.bannerFont; + _ = this.bannerFont.Value; } private enum State From 47902f977040014586d93b64c126ffe6c7166089 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 21:53:45 +0900 Subject: [PATCH 11/71] Guarantee rounding advanceX/kerning pair distances --- .../Internals/FontAtlasFactory.BuildToolkit.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 4403d6400..4fa4c6a9e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -426,7 +426,9 @@ internal sealed partial class FontAtlasFactory var scale = this.Scale; foreach (ref var font in this.Fonts.DataSpan) { - if (!this.GlobalScaleExclusions.Contains(font) && Math.Abs(scale - 1f) > 0f) + if (this.GlobalScaleExclusions.Contains(font)) + font.AdjustGlyphMetrics(1f, 1f); // we still need to round advanceX and kerning + else font.AdjustGlyphMetrics(1 / scale, scale); foreach (var c in FallbackCodepoints) From 7eb4bf8ab46a7d2e4b9528b5528d2621a7595e50 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 22:16:12 +0900 Subject: [PATCH 12/71] Add some more examples to doc comments --- Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs | 2 +- .../ManagedFontAtlas/Internals/DelegateFontHandle.cs | 6 +++--- .../Internals/FontAtlasFactory.Implementation.cs | 4 ++-- Dalamud/Interface/UiBuilder.cs | 12 ++++++++---- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index 6d971dc02..0a50d6070 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -58,7 +58,7 @@ public interface IFontAtlas : IDisposable public IFontHandle NewGameFontHandle(GameFontStyle style); /// - public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate @delegate); + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate); /// public void FreeFontHandle(IFontHandle handle); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index 142bd73da..f9f2c0ef1 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -91,11 +91,11 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// /// Creates a new IFontHandle using your own callbacks. /// - /// Callback for . + /// Callback for . /// Handle to a font that may or may not be ready yet. - public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate callOnBuildStepChange) + public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) { - var key = new DelegateFontHandle(this, callOnBuildStepChange); + var key = new DelegateFontHandle(this, buildStepDelegate); lock (this.syncRoot) this.handles.Add(key); this.RebuildRecommend?.Invoke(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 3f0b5b22e..52d77b963 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -360,8 +360,8 @@ internal sealed partial class FontAtlasFactory public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); /// - public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate @delegate) => - this.delegateFontHandleManager.NewFontHandle(@delegate); + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => + this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); /// public void FreeFontHandle(IFontHandle handle) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index f7beb22fa..5d0810009 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -428,18 +428,22 @@ public sealed class UiBuilder : IDisposable /// /// - /// On initialization: + /// On initialization: /// /// this.fontHandle = uiBuilder.NewDelegateFontHandle(e => e.OnPreBuild(tk => { /// var config = new SafeFontConfig { SizePx = 16 }; /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); /// tk.AddGameSymbol(config); /// tk.AddExtraGlyphsForDalamudLanguage(config); - /// // optional: tk.Font = config.MergeFont; + /// // optionally do the following if you have to add more than one font here, + /// // to specify which font added during this delegate is the final font to use. + /// tk.Font = config.MergeFont; /// })); + /// // or + /// this.fontHandle = uiBuilder.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); /// - /// - /// On use: + ///
+ /// On use: /// /// using (this.fontHandle.Push()) /// ImGui.TextUnformatted("Example"); From e7c7cdaa2975c966b1e3e9c682509bd2c8fb50cd Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 14:39:22 +0900 Subject: [PATCH 13/71] Update docs and exposed API --- .../Interface/Internal/InterfaceManager.cs | 57 +++++++++-------- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 60 ++++++++++++++++-- .../Internals/DelegateFontHandle.cs | 6 +- .../FontAtlasFactory.Implementation.cs | 33 +++++++--- .../Internals/GamePrebakedFontHandle.cs | 6 +- Dalamud/Interface/UiBuilder.cs | 61 ++++++------------- 6 files changed, 128 insertions(+), 95 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 46d37fe90..d252321db 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -699,35 +699,38 @@ internal class InterfaceManager : IDisposable, IServiceType { this.dalamudAtlas = fontAtlasFactory .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); - this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); - this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild( - tk => tk.AddFontAwesomeIconFont( - new() - { - SizePx = DefaultFontSizePx, - GlyphMinAdvanceX = DefaultFontSizePx, - GlyphMaxAdvanceX = DefaultFontSizePx, - }))); - this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild( - tk => tk.AddDalamudAssetFont( - DalamudAsset.InconsolataRegular, - new() { SizePx = DefaultFontSizePx }))); - this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( - tk => - { - // Note: the first call of this function is done outside the main thread; this is expected. - // Do not use DefaultFont, IconFont, and MonoFont. - // Use font handles directly. + using (this.dalamudAtlas.SuppressAutoRebuild()) + { + this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); + this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddFontAwesomeIconFont( + new() + { + SizePx = DefaultFontSizePx, + GlyphMinAdvanceX = DefaultFontSizePx, + GlyphMaxAdvanceX = DefaultFontSizePx, + }))); + this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddDalamudAssetFont( + DalamudAsset.InconsolataRegular, + new() { SizePx = DefaultFontSizePx }))); + this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( + tk => + { + // Note: the first call of this function is done outside the main thread; this is expected. + // Do not use DefaultFont, IconFont, and MonoFont. + // Use font handles directly. - // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); + // Fill missing glyphs in MonoFont from DefaultFont + tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); - // Broadcast to auto-rebuilding instances - this.AfterBuildFonts?.Invoke(); - }); + // Broadcast to auto-rebuilding instances + this.AfterBuildFonts?.Invoke(); + }); + } // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. _ = this.dalamudAtlas.BuildFontsAsync(false); diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index 0a50d6070..d32adc1eb 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas.Internals; using ImGuiNET; @@ -54,25 +53,73 @@ public interface IFontAtlas : IDisposable ///
bool IsGlobalScaled { get; } - /// + /// + /// Suppresses automatically rebuilding fonts for the scope. + /// + /// An instance of that will release the suppression. + /// + /// Use when you will be creating multiple new handles, and want rebuild to trigger only when you're done doing so. + /// This function will effectively do nothing, if is set to + /// . + /// + /// + /// + /// using (atlas.SuppressBuild()) { + /// this.font1 = atlas.NewGameFontHandle(...); + /// this.font2 = atlas.NewDelegateFontHandle(...); + /// } + /// + /// + public IDisposable SuppressAutoRebuild(); + + /// + /// Creates a new from game's built-in fonts. + /// + /// Font to use. + /// Handle to a font that may or may not be ready yet. public IFontHandle NewGameFontHandle(GameFontStyle style); - /// + /// + /// Creates a new IFontHandle using your own callbacks. + /// + /// Callback for . + /// Handle to a font that may or may not be ready yet. + /// + /// On initialization: + /// + /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => { + /// var config = new SafeFontConfig { SizePx = 16 }; + /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); + /// tk.AddGameSymbol(config); + /// tk.AddExtraGlyphsForDalamudLanguage(config); + /// // optionally do the following if you have to add more than one font here, + /// // to specify which font added during this delegate is the final font to use. + /// tk.Font = config.MergeFont; + /// })); + /// // or + /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); + /// + ///
+ /// On use: + /// + /// using (this.fontHandle.Push()) + /// ImGui.TextUnformatted("Example"); + /// + ///
public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate); - /// - public void FreeFontHandle(IFontHandle handle); - /// /// Queues rebuilding fonts, on the main thread.
/// Note that would not necessarily get changed from calling this function. ///
+ /// If is . void BuildFontsOnNextFrame(); /// /// Rebuilds fonts immediately, on the current thread.
/// Even the callback for will be called on the same thread. ///
+ /// If is . void BuildFontsImmediately(); /// @@ -80,5 +127,6 @@ public interface IFontAtlas : IDisposable /// /// Call on the main thread. /// The task. + /// If is . Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index f9f2c0ef1..b6ec720dc 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -88,11 +88,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal } } - /// - /// Creates a new IFontHandle using your own callbacks. - /// - /// Callback for . - /// Handle to a font that may or may not be ready yet. + /// public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) { var key = new DelegateFontHandle(this, buildStepDelegate); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 52d77b963..5656fc673 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; @@ -203,6 +204,9 @@ internal sealed partial class FontAtlasFactory private Task buildTask = EmptyTask; private FontAtlasBuiltData builtData; + private int buildSuppressionCounter; + private bool buildSuppressionSuppressed; + private int buildIndex; private bool buildQueued; private bool disposed = false; @@ -356,6 +360,19 @@ internal sealed partial class FontAtlasFactory GC.SuppressFinalize(this); } + /// + public IDisposable SuppressAutoRebuild() + { + this.buildSuppressionCounter++; + return Disposable.Create( + () => + { + this.buildSuppressionCounter--; + if (this.buildSuppressionSuppressed) + this.OnRebuildRecommend(); + }); + } + /// public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); @@ -363,15 +380,6 @@ internal sealed partial class FontAtlasFactory public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); - /// - public void FreeFontHandle(IFontHandle handle) - { - foreach (var manager in this.fontHandleManagers) - { - manager.FreeFontHandle(handle); - } - } - /// public void BuildFontsOnNextFrame() { @@ -688,6 +696,13 @@ internal sealed partial class FontAtlasFactory if (this.disposed) return; + if (this.buildSuppressionCounter > 0) + { + this.buildSuppressionSuppressed = true; + return; + } + + this.buildSuppressionSuppressed = false; this.factory.Framework.RunOnFrameworkThread( () => { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index c40302f6c..2739ed2da 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -157,11 +157,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal this.Substance = null; } - /// - /// Creates a new from game's built-in fonts. - /// - /// Font to use. - /// Handle to a font that may or may not be ready yet. + /// public IFontHandle NewFontHandle(GameFontStyle style) { var handle = new GamePrebakedFontHandle(this, style); diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 5d0810009..a477ec09e 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -37,7 +37,6 @@ public sealed class UiBuilder : IDisposable private readonly DalamudConfiguration configuration = Service.Get(); private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private readonly IFontAtlas privateAtlas; private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -61,14 +60,14 @@ public sealed class UiBuilder : IDisposable this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); - this.privateAtlas = + this.FontAtlas = this.scopedFinalizer .Add( Service .Get() .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); - this.privateAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; - this.privateAtlas.RebuildRecommend += this.RebuildFonts; + this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; + this.FontAtlas.RebuildRecommend += this.RebuildFonts; } catch { @@ -104,7 +103,7 @@ public sealed class UiBuilder : IDisposable /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
- [Obsolete($"Use {nameof(NewDelegateFontHandle)} instead.", false)] + [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] public event Action? BuildFonts; /// @@ -113,7 +112,7 @@ public sealed class UiBuilder : IDisposable /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. /// - [Obsolete($"Use {nameof(NewDelegateFontHandle)} instead.", false)] + [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] public event Action? AfterBuildFonts; /// @@ -145,7 +144,7 @@ public sealed class UiBuilder : IDisposable /// /// A font handle corresponding to this font can be obtained with: /// - /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// fontAtlas.NewDelegateFontHandle( /// e => e.OnPreBuild( /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); /// @@ -159,7 +158,7 @@ public sealed class UiBuilder : IDisposable /// /// A font handle corresponding to this font can be obtained with: /// - /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// fontAtlas.NewDelegateFontHandle( /// e => e.OnPreBuild( /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); /// @@ -173,7 +172,7 @@ public sealed class UiBuilder : IDisposable /// /// A font handle corresponding to this font can be obtained with: /// - /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// fontAtlas.NewDelegateFontHandle( /// e => e.OnPreBuild( /// tk => tk.AddDalamudAssetFont( /// DalamudAsset.InconsolataRegular, @@ -251,6 +250,11 @@ public sealed class UiBuilder : IDisposable /// public bool UiPrepared => Service.GetNullable() != null; + /// + /// Gets the plugin-private font atlas. + /// + public IFontAtlas FontAtlas { get; } + /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// @@ -418,40 +422,11 @@ public sealed class UiBuilder : IDisposable /// /// Font to get. /// Handle to the game font which may or may not be available for use yet. - [Obsolete($"Use {nameof(NewGameFontHandle)} instead.", false)] + [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( - (IFontHandle.IInternal)this.NewGameFontHandle(style), + (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), Service.Get()); - /// - public IFontHandle NewGameFontHandle(GameFontStyle style) => this.privateAtlas.NewGameFontHandle(style); - - /// - /// - /// On initialization: - /// - /// this.fontHandle = uiBuilder.NewDelegateFontHandle(e => e.OnPreBuild(tk => { - /// var config = new SafeFontConfig { SizePx = 16 }; - /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); - /// tk.AddGameSymbol(config); - /// tk.AddExtraGlyphsForDalamudLanguage(config); - /// // optionally do the following if you have to add more than one font here, - /// // to specify which font added during this delegate is the final font to use. - /// tk.Font = config.MergeFont; - /// })); - /// // or - /// this.fontHandle = uiBuilder.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); - /// - ///
- /// On use: - /// - /// using (this.fontHandle.Push()) - /// ImGui.TextUnformatted("Example"); - /// - ///
- public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => - this.privateAtlas.NewDelegateFontHandle(buildStepDelegate); - /// /// Call this to queue a rebuild of the font atlas.
/// This will invoke any and handlers and ensure that any @@ -461,9 +436,9 @@ public sealed class UiBuilder : IDisposable { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); if (this.AfterBuildFonts is null && this.BuildFonts is null) - this.privateAtlas.BuildFontsAsync(); + this.FontAtlas.BuildFontsAsync(); else - this.privateAtlas.BuildFontsOnNextFrame(); + this.FontAtlas.BuildFontsOnNextFrame(); } /// @@ -579,7 +554,7 @@ public sealed class UiBuilder : IDisposable } // just in case, if something goes wrong, prevent drawing; otherwise it probably will crash. - if (!this.privateAtlas.BuildTask.IsCompletedSuccessfully + if (!this.FontAtlas.BuildTask.IsCompletedSuccessfully && (this.BuildFonts is not null || this.AfterBuildFonts is not null)) { return; From 77536429d641a897e36f858da77ec9782af791ec Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 15:25:12 +0900 Subject: [PATCH 14/71] Fix adding supplemental language fonts for GamePrebakedFontHandle --- .../FontAtlasBuildToolkitUtilities.cs | 22 ++++++++++++ .../Internals/GamePrebakedFontHandle.cs | 34 ++++++++++++------- .../ManagedFontAtlas/SafeFontConfig.cs | 11 ++++++ Dalamud/Interface/Utility/ImGuiHelpers.cs | 1 + 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs index d12409d51..586887a3b 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; +using System.Runtime.CompilerServices; using Dalamud.Interface.Utility; +using ImGuiNET; + namespace Dalamud.Interface.ManagedFontAtlas; /// @@ -58,6 +61,25 @@ public static class FontAtlasBuildToolkitUtilities bool addEllipsisCodepoints = true) => @string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints); + /// + /// Finds the corresponding in + /// . that corresponds to the + /// specified font . + /// + /// The toolkit. + /// The font. + /// The relevant config pointer, or empty config pointer if not found. + public static unsafe ImFontConfigPtr FindConfigPtr(this IFontAtlasBuildToolkit toolkit, ImFontPtr fontPtr) + { + foreach (ref var c in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan) + { + if (c.DstFont == fontPtr.NativePtr) + return new((nint)Unsafe.AsPointer(ref c)); + } + + return default; + } + /// /// Invokes /// if of diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 2739ed2da..7e9ef9019 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -204,7 +204,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal internal sealed class HandleSubstance : IFontHandleSubstance { private readonly HandleManager handleManager; - private readonly InterfaceManager interfaceManager; private readonly HashSet gameFontStyles; // Owned by this class, but ImFontPtr values still do not belong to this. @@ -226,7 +225,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) { this.handleManager = manager; - this.interfaceManager = Service.Get(); + Service.Get(); this.gameFontStyles = new(gameFontStyles); } @@ -351,20 +350,21 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal var fdtGlyphs = fdt.Glyphs; var fontPtr = font.NativePtr; - fontPtr->FontSize = (fdtFontHeader.Size * 4) / 3; + var glyphs = font.GlyphsWrapped(); + var scale = toolkitPostBuild.Scale * (style.SizePt / fdtFontHeader.Size); + + fontPtr->FontSize = toolkitPostBuild.Scale * style.SizePx; if (fontPtr->ConfigData != null) fontPtr->ConfigData->SizePixels = fontPtr->FontSize; - fontPtr->Ascent = fdtFontHeader.Ascent; - fontPtr->Descent = fdtFontHeader.Descent; + fontPtr->Ascent = fdtFontHeader.Ascent * scale; + fontPtr->Descent = fdtFontHeader.Descent * scale; fontPtr->EllipsisChar = '…'; if (!allTexFiles.TryGetValue(attr.TexPathFormat, out var texFiles)) allTexFiles.Add(attr.TexPathFormat, texFiles = ArrayPool.Shared.Rent(texCount)); - + if (this.glyphRectIds.TryGetValue(style, out var rectIdToGlyphs)) { - this.glyphRectIds.Remove(style); - foreach (var (rectId, fdtGlyphIndex) in rectIdToGlyphs.Values) { ref var glyph = ref fdtGlyphs[fdtGlyphIndex]; @@ -442,6 +442,9 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } } + + glyphs[rc->GlyphId].XY *= scale; + glyphs[rc->GlyphId].AdvanceX *= scale; } } else if (this.lateBuildRanges.TryGetValue(font, out var buildRanges)) @@ -480,7 +483,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal textureIndices.AsSpan(0, texCount).Fill(-1); } - var glyphs = font.GlyphsWrapped(); glyphs.EnsureCapacity(glyphs.Length + buildRanges.Sum(x => (x.To - x.From) + 1)); foreach (var (rangeMin, rangeMax) in buildRanges) { @@ -530,6 +532,8 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal glyph.XY1 = glyph.XY0 + glyph.UV1; glyph.UV1 += glyph.UV0; glyph.UV /= fdtTexSize; + glyph.XY *= scale; + glyph.AdvanceX *= scale; glyphs.Add(glyph); } @@ -555,7 +559,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } - font.AdjustGlyphMetrics(style.SizePt / fdtFontHeader.Size, toolkitPostBuild.Scale); + font.AdjustGlyphMetrics(1 / toolkitPostBuild.Scale, toolkitPostBuild.Scale); } catch (Exception e) { @@ -616,7 +620,10 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal var font = toolkitPreBuild.IgnoreGlobalScale(this.CreateTemplateFont(style, toolkitPreBuild)); if (addExtraLanguageGlyphs) - toolkitPreBuild.AddExtraGlyphsForDalamudLanguage(new() { MergeFont = font }); + { + toolkitPreBuild.AddExtraGlyphsForDalamudLanguage( + new(toolkitPreBuild.FindConfigPtr(font)) { MergeFont = font }); + } var fas = GameFontStyle.GetRecommendedFamilyAndSize(style.Family, style.SizePt * toolkitPreBuild.Scale); var horizontalOffset = fas.GetAttribute()?.HorizontalOffset ?? 0; @@ -662,9 +669,10 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal fdtGlyphIndex); } } - + + var scale = toolkitPreBuild.Scale * (style.SizePt / fdt.FontHeader.Size); foreach (ref var kernPair in fdt.PairAdjustments) - font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); + font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset * scale); return font; } diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs index 812608973..cd840e5ed 100644 --- a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -31,6 +31,17 @@ public struct SafeFontConfig this.Raw.FontDataOwnedByAtlas = 1; } + /// + /// Initializes a new instance of the struct, + /// copying applicable values from an existing instance of . + /// + /// Config to copy from. + public unsafe SafeFontConfig(ImFontConfigPtr config) + { + this.Raw = *config.NativePtr; + this.Raw.GlyphRanges = null; + } + /// /// Gets or sets the index of font within a TTF/OTF file. /// diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 80329f558..ed6ad1dfe 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -525,6 +525,7 @@ public static class ImGuiHelpers (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), }) + .Append((ushort)0) .ToArray(); /// From 3d576a0654bb947cc6dbaa43fc4a7ff6610913cf Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 15:40:58 +0900 Subject: [PATCH 15/71] Fix inconsistencies --- .../Internals/FontAtlasFactory.BuildToolkit.cs | 6 +----- .../ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 4fa4c6a9e..a3abc6681 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -398,8 +398,6 @@ internal sealed partial class FontAtlasFactory config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue; config.GlyphOffset *= this.Scale; - - config.RasterizerGamma *= 1.4f; } } @@ -426,9 +424,7 @@ internal sealed partial class FontAtlasFactory var scale = this.Scale; foreach (ref var font in this.Fonts.DataSpan) { - if (this.GlobalScaleExclusions.Contains(font)) - font.AdjustGlyphMetrics(1f, 1f); // we still need to round advanceX and kerning - else + if (!this.GlobalScaleExclusions.Contains(font)) font.AdjustGlyphMetrics(1 / scale, scale); foreach (var c in FallbackCodepoints) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 7e9ef9019..040f9a743 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -621,8 +621,12 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal if (addExtraLanguageGlyphs) { - toolkitPreBuild.AddExtraGlyphsForDalamudLanguage( - new(toolkitPreBuild.FindConfigPtr(font)) { MergeFont = font }); + var cfg = toolkitPreBuild.FindConfigPtr(font); + toolkitPreBuild.AddExtraGlyphsForDalamudLanguage(new() + { + MergeFont = cfg.DstFont, + SizePx = cfg.SizePixels, + }); } var fas = GameFontStyle.GetRecommendedFamilyAndSize(style.Family, style.SizePt * toolkitPreBuild.Scale); From d78667900f4f509b1cdd0fa964f3bd0346385aca Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 21:08:35 +0900 Subject: [PATCH 16/71] Make it possible to attach arbitrary game font from delegate font --- Dalamud/Interface/GameFonts/FdtFileView.cs | 2 +- Dalamud/Interface/GameFonts/GameFontStyle.cs | 35 +- .../IFontAtlasBuildToolkitPreBuild.cs | 27 +- .../Internals/DelegateFontHandle.cs | 6 + .../FontAtlasFactory.BuildToolkit.cs | 65 +- .../Internals/GamePrebakedFontHandle.cs | 801 ++++++++++-------- .../Internals/IFontHandleSubstance.cs | 7 + .../ManagedFontAtlas/SafeFontConfig.cs | 8 +- 8 files changed, 544 insertions(+), 407 deletions(-) diff --git a/Dalamud/Interface/GameFonts/FdtFileView.cs b/Dalamud/Interface/GameFonts/FdtFileView.cs index 78b2e22f3..896a6dbb4 100644 --- a/Dalamud/Interface/GameFonts/FdtFileView.cs +++ b/Dalamud/Interface/GameFonts/FdtFileView.cs @@ -6,7 +6,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Reference member view of a .fdt file data. /// -internal readonly unsafe ref struct FdtFileView +internal readonly unsafe struct FdtFileView { private readonly byte* ptr; diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index e219670b8..fbaf9de07 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -64,7 +64,7 @@ public struct GameFontStyle /// public float SizePt { - get => this.SizePx * 3 / 4; + readonly get => this.SizePx * 3 / 4; set => this.SizePx = value * 4 / 3; } @@ -73,14 +73,14 @@ public struct GameFontStyle /// public float BaseSkewStrength { - get => this.SkewStrength * this.BaseSizePx / this.SizePx; + readonly get => this.SkewStrength * this.BaseSizePx / this.SizePx; set => this.SkewStrength = value * this.SizePx / this.BaseSizePx; } /// /// Gets the font family. /// - public GameFontFamily Family => this.FamilyAndSize switch + public readonly GameFontFamily Family => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined, GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis, @@ -112,7 +112,7 @@ public struct GameFontStyle /// /// Gets the corresponding GameFontFamilyAndSize but with minimum possible font sizes. /// - public GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch + public readonly GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch { GameFontFamily.Axis => GameFontFamilyAndSize.Axis96, GameFontFamily.Jupiter => GameFontFamilyAndSize.Jupiter16, @@ -126,7 +126,7 @@ public struct GameFontStyle /// /// Gets the base font size in point unit. /// - public float BaseSizePt => this.FamilyAndSize switch + public readonly float BaseSizePt => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => 0, GameFontFamilyAndSize.Axis96 => 9.6f, @@ -158,14 +158,14 @@ public struct GameFontStyle /// /// Gets the base font size in pixel unit. /// - public float BaseSizePx => this.BaseSizePt * 4 / 3; + public readonly float BaseSizePx => this.BaseSizePt * 4 / 3; /// /// Gets or sets a value indicating whether this font is bold. /// public bool Bold { - get => this.Weight > 0f; + readonly get => this.Weight > 0f; set => this.Weight = value ? 1f : 0f; } @@ -174,7 +174,7 @@ public struct GameFontStyle /// public bool Italic { - get => this.SkewStrength != 0; + readonly get => this.SkewStrength != 0; set => this.SkewStrength = value ? this.SizePx / 6 : 0; } @@ -233,13 +233,26 @@ public struct GameFontStyle _ => GameFontFamilyAndSize.Undefined, }; + /// + /// Creates a new scaled instance of struct. + /// + /// The scale. + /// The scaled instance. + public readonly GameFontStyle Scale(float scale) => new() + { + FamilyAndSize = GetRecommendedFamilyAndSize(this.Family, this.SizePt * scale), + SizePx = this.SizePx * scale, + Weight = this.Weight, + SkewStrength = this.SkewStrength * scale, + }; + /// /// Calculates the adjustment to width resulting fron Weight and SkewStrength. /// /// Font header. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + public readonly int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) { var widthDelta = this.Weight; switch (this.BaseSkewStrength) @@ -263,11 +276,11 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + public readonly int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// - public override string ToString() + public override readonly string ToString() { return $"GameFontStyle({this.FamilyAndSize}, {this.SizePt}pt, skew={this.SkewStrength}, weight={this.Weight})"; } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index dbe8626e9..cb8a27a54 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -1,6 +1,7 @@ using System.IO; using System.Runtime.InteropServices; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; using ImGuiNET; @@ -44,6 +45,13 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// Same with . ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr); + /// + /// Gets whether global scaling is ignored for the given font. + /// + /// The font. + /// True if ignored. + bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + /// /// Adds a font from memory region allocated using .
/// It WILL crash if you try to use a memory pointer allocated in some other way.
@@ -120,7 +128,7 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds the default font known to the current font atlas.
///
- /// Includes and .
+ /// Includes and .
/// As this involves adding multiple fonts, calling this function will set /// as the return value of this function, if it was empty before. ///
@@ -153,15 +161,26 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds the game's symbols into the provided font.
- /// will be ignored. + /// will be ignored.
+ /// If the game symbol font file is unavailable, only will be honored. ///
/// The font config. - void AddGameSymbol(in SafeFontConfig fontConfig); + /// The added font. + ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig); + + /// + /// Adds the game glyphs to the font. + /// + /// The font style. + /// The glyph ranges. + /// The font to merge to. If empty, then a new font will be created. + /// The added font. + ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont); /// /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
/// will be ignored. ///
/// The font config. - void AddExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); + void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index b6ec720dc..f0ed09155 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -271,6 +271,12 @@ internal class DelegateFontHandle : IFontHandle.IInternal } } + /// + public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + // irrelevant + } + /// public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index a3abc6681..46fb3f63d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -273,24 +273,21 @@ internal sealed partial class FontAtlasFactory /// public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges) { + ImFontPtr font; + glyphRanges ??= this.factory.DefaultGlyphRanges; if (Service.Get().UseAxis) { - return this.gameFontHandleSubstance.GetOrCreateFont( - new(GameFontFamily.Axis, sizePx), - this); + font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); + } + else + { + font = this.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() { SizePx = sizePx, GlyphRanges = glyphRanges }); + this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font }); } - glyphRanges ??= this.factory.DefaultGlyphRanges; - - var fontConfig = new SafeFontConfig - { - SizePx = sizePx, - GlyphRanges = glyphRanges, - }; - - var font = this.AddDalamudAssetFont(DalamudAsset.NotoSansJpMedium, fontConfig); - this.AddExtraGlyphsForDalamudLanguage(fontConfig with { MergeFont = font }); - this.AddGameSymbol(fontConfig with { MergeFont = font }); + this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font }); if (this.Font.IsNull()) this.Font = font; return font; @@ -315,11 +312,12 @@ internal sealed partial class FontAtlasFactory }); case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile: - return this.gameFontHandleSubstance.AttachGameSymbols( - this, - fontConfig.MergeFont, - fontConfig.SizePx, - fontConfig.GlyphRanges); + { + return this.AddGameGlyphs( + new(GameFontFamily.Axis, fontConfig.SizePx), + fontConfig.GlyphRanges, + fontConfig.MergeFont); + } default: return this.factory.AddFont( @@ -341,20 +339,25 @@ internal sealed partial class FontAtlasFactory }); /// - public void AddGameSymbol(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( - DalamudAsset.LodestoneGameSymbol, - fontConfig with - { - GlyphRanges = new ushort[] + public ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig) => + this.AddDalamudAssetFont( + DalamudAsset.LodestoneGameSymbol, + fontConfig with { - GamePrebakedFontHandle.SeIconCharMin, - GamePrebakedFontHandle.SeIconCharMax, - 0, - }, - }); + GlyphRanges = new ushort[] + { + GamePrebakedFontHandle.SeIconCharMin, + GamePrebakedFontHandle.SeIconCharMax, + 0, + }, + }); /// - public void AddExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) + public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) => + this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges); + + /// + public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) { var dalamudConfiguration = Service.Get(); if (dalamudConfiguration.EffectiveLanguage == "ko") @@ -377,6 +380,8 @@ internal sealed partial class FontAtlasFactory { foreach (var substance in this.data.Substances) substance.OnPreBuild(this); + foreach (var substance in this.data.Substances) + substance.OnPreBuildCleanup(this); } public unsafe void PreBuild() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 040f9a743..1ac6fdbce 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -1,5 +1,7 @@ using System.Buffers; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Disposables; @@ -207,15 +209,11 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal private readonly HashSet gameFontStyles; // Owned by this class, but ImFontPtr values still do not belong to this. - private readonly Dictionary fonts = new(); + private readonly Dictionary fonts = new(); private readonly Dictionary buildExceptions = new(); - private readonly Dictionary> fontCopyTargets = new(); + private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new(); private readonly HashSet templatedFonts = new(); - private readonly Dictionary> lateBuildRanges = new(); - - private readonly Dictionary> glyphRectIds = - new(); /// /// Initializes a new instance of the class. @@ -238,29 +236,22 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } /// - /// Attaches game symbols to the given font. + /// Attaches game symbols to the given font. If font is null, it will be created. /// /// The toolkitPostBuild. /// The font to attach to. - /// The font size in pixels. + /// The game font style. /// The intended glyph ranges. /// if it is not empty; otherwise a new font. - public ImFontPtr AttachGameSymbols( + public ImFontPtr AttachGameGlyphs( IFontAtlasBuildToolkitPreBuild toolkitPreBuild, ImFontPtr font, - float sizePx, - ushort[]? glyphRanges) + GameFontStyle style, + ushort[]? glyphRanges = null) { - var style = new GameFontStyle(GameFontFamily.Axis, sizePx); - var referenceFont = this.GetOrCreateFont(style, toolkitPreBuild); - if (font.IsNull()) - font = this.CreateTemplateFont(style, toolkitPreBuild); - - if (!this.fontCopyTargets.TryGetValue(referenceFont, out var copyTargets)) - this.fontCopyTargets[referenceFont] = copyTargets = new(); - - copyTargets.Add((font, glyphRanges)); + font = this.CreateTemplateFont(toolkitPreBuild, style.SizePx); + this.attachments.Add((font, style, glyphRanges)); return font; } @@ -272,14 +263,20 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// The font. public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) { - if (this.fonts.TryGetValue(style, out var font)) - return font; - try { - font = this.CreateFontPrivate(style, toolkitPreBuild, ' ', '\uFFFE', true); - this.fonts.Add(style, font); - return font; + if (!this.fonts.TryGetValue(style, out var plan)) + { + plan = new( + style, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + this.fonts[style] = plan; + } + + plan.AttachFont(plan.FullRangeFont); + return plan.FullRangeFont; } catch (Exception e) { @@ -290,7 +287,9 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public ImFontPtr GetFontPtr(IFontHandle handle) => - handle is GamePrebakedFontHandle ggfh ? this.fonts.GetValueOrDefault(ggfh.FontStyle) : default; + handle is GamePrebakedFontHandle ggfh + ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default + : default; /// public Exception? GetBuildException(IFontHandle handle) => @@ -315,6 +314,34 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } + /// + public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + foreach (var (font, style, ranges) in this.attachments) + { + var effectiveStyle = + toolkitPreBuild.IsGlobalScaleIgnored(font) + ? style.Scale(1 / toolkitPreBuild.Scale) + : style; + if (!this.fonts.TryGetValue(style, out var plan)) + { + plan = new( + effectiveStyle, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + this.fonts[style] = plan; + } + + plan.AttachFont(font, ranges); + } + + foreach (var plan in this.fonts.Values) + { + plan.EnsureGlyphs(toolkitPreBuild.NewImAtlas); + } + } + /// public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) { @@ -331,235 +358,19 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; - var heights = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; for (var i = 0; i < pixels8Array.Length; i++) - toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out heights[i]); + toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out _); - foreach (var (style, font) in this.fonts) + foreach (var (style, plan) in this.fonts) { try { - var fas = GameFontStyle.GetRecommendedFamilyAndSize( - style.Family, - style.SizePt * toolkitPostBuild.Scale); - var attr = fas.GetAttribute(); - var horizontalOffset = attr?.HorizontalOffset ?? 0; - var texCount = this.handleManager.GameFontTextureProvider.GetFontTextureCount(attr.TexPathFormat); - using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); - ref var fdtFontHeader = ref fdt.FontHeader; - var fdtGlyphs = fdt.Glyphs; - var fontPtr = font.NativePtr; + foreach (var font in plan.Ranges.Keys) + this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale); - var glyphs = font.GlyphsWrapped(); - var scale = toolkitPostBuild.Scale * (style.SizePt / fdtFontHeader.Size); - - fontPtr->FontSize = toolkitPostBuild.Scale * style.SizePx; - if (fontPtr->ConfigData != null) - fontPtr->ConfigData->SizePixels = fontPtr->FontSize; - fontPtr->Ascent = fdtFontHeader.Ascent * scale; - fontPtr->Descent = fdtFontHeader.Descent * scale; - fontPtr->EllipsisChar = '…'; - - if (!allTexFiles.TryGetValue(attr.TexPathFormat, out var texFiles)) - allTexFiles.Add(attr.TexPathFormat, texFiles = ArrayPool.Shared.Rent(texCount)); - - if (this.glyphRectIds.TryGetValue(style, out var rectIdToGlyphs)) - { - foreach (var (rectId, fdtGlyphIndex) in rectIdToGlyphs.Values) - { - ref var glyph = ref fdtGlyphs[fdtGlyphIndex]; - var rc = (ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas - .GetCustomRectByIndex(rectId) - .NativePtr; - var pixels8 = pixels8Array[rc->TextureIndex]; - var width = widths[rc->TextureIndex]; - texFiles[glyph.TextureFileIndex] ??= - this.handleManager - .GameFontTextureProvider - .GetTexFile(attr.TexPathFormat, glyph.TextureFileIndex); - var sourceBuffer = texFiles[glyph.TextureFileIndex].ImageData; - var sourceBufferDelta = glyph.TextureChannelByteIndex; - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdtFontHeader, glyph); - if (widthAdjustment == 0) - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var a = sourceBuffer[ - sourceBufferDelta + - (4 * (((glyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + - glyph.TextureOffsetX + x))]; - pixels8[((rc->Y + y) * width) + rc->X + x] = a; - } - } - } - else - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) - pixels8[((rc->Y + y) * width) + rc->X + x] = 0; - } - - for (int xbold = 0, xboldTo = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); - xbold < xboldTo; - xbold++) - { - var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); - for (var y = 0; y < glyph.BoundingHeight; y++) - { - float xDelta = xbold; - if (style.BaseSkewStrength > 0) - { - xDelta += style.BaseSkewStrength * - (fdtFontHeader.LineHeight - glyph.CurrentOffsetY - y) / - fdtFontHeader.LineHeight; - } - else if (style.BaseSkewStrength < 0) - { - xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / - fdtFontHeader.LineHeight; - } - - var xDeltaInt = (int)Math.Floor(xDelta); - var xness = xDelta - xDeltaInt; - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var sourcePixelIndex = - ((glyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + - glyph.TextureOffsetX + x; - var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; - var a2 = x == glyph.BoundingWidth - 1 - ? 0 - : sourceBuffer[sourceBufferDelta - + (4 * (sourcePixelIndex + 1))]; - var n = (a1 * xness) + (a2 * (1 - xness)); - var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; - pixels8[targetOffset] = - Math.Max(pixels8[targetOffset], (byte)(boldStrength * n)); - } - } - } - } - - glyphs[rc->GlyphId].XY *= scale; - glyphs[rc->GlyphId].AdvanceX *= scale; - } - } - else if (this.lateBuildRanges.TryGetValue(font, out var buildRanges)) - { - buildRanges.Sort(); - for (var i = 0; i < buildRanges.Count; i++) - { - var current = buildRanges[i]; - if (current.From > current.To) - buildRanges[i] = (From: current.To, To: current.From); - } - - for (var i = 0; i < buildRanges.Count - 1; i++) - { - var current = buildRanges[i]; - var next = buildRanges[i + 1]; - if (next.From <= current.To) - { - buildRanges[i] = current with { To = next.To }; - buildRanges.RemoveAt(i + 1); - i--; - } - } - - var fdtTexSize = new Vector4( - fdtFontHeader.TextureWidth, - fdtFontHeader.TextureHeight, - fdtFontHeader.TextureWidth, - fdtFontHeader.TextureHeight); - - if (!allTextureIndices.TryGetValue(attr.TexPathFormat, out var textureIndices)) - { - allTextureIndices.Add( - attr.TexPathFormat, - textureIndices = ArrayPool.Shared.Rent(texCount)); - textureIndices.AsSpan(0, texCount).Fill(-1); - } - - glyphs.EnsureCapacity(glyphs.Length + buildRanges.Sum(x => (x.To - x.From) + 1)); - foreach (var (rangeMin, rangeMax) in buildRanges) - { - var glyphIndex = fdt.FindGlyphIndex(rangeMin); - if (glyphIndex < 0) - glyphIndex = ~glyphIndex; - var endIndex = fdt.FindGlyphIndex(rangeMax); - if (endIndex < 0) - endIndex = ~endIndex - 1; - for (; glyphIndex <= endIndex; glyphIndex++) - { - var fdtg = fdtGlyphs[glyphIndex]; - - // If the glyph already exists in the target font, we do not overwrite. - if ( - !(fdtg.Char == ' ' && this.templatedFonts.Contains(font)) - && font.FindGlyphNoFallback(fdtg.Char).NativePtr is not null) - { - continue; - } - - ref var textureIndex = ref textureIndices[fdtg.TextureIndex]; - if (textureIndex == -1) - { - textureIndex = toolkitPostBuild.StoreTexture( - this.handleManager - .GameFontTextureProvider - .NewFontTextureRef(attr.TexPathFormat, fdtg.TextureIndex), - true); - } - - var glyph = new ImGuiHelpers.ImFontGlyphReal - { - AdvanceX = fdtg.AdvanceWidth, - Codepoint = fdtg.Char, - Colored = false, - TextureIndex = textureIndex, - Visible = true, - X0 = horizontalOffset, - Y0 = fdtg.CurrentOffsetY, - U0 = fdtg.TextureOffsetX, - V0 = fdtg.TextureOffsetY, - U1 = fdtg.BoundingWidth, - V1 = fdtg.BoundingHeight, - }; - - glyph.XY1 = glyph.XY0 + glyph.UV1; - glyph.UV1 += glyph.UV0; - glyph.UV /= fdtTexSize; - glyph.XY *= scale; - glyph.AdvanceX *= scale; - - glyphs.Add(glyph); - } - } - - font.NativePtr->FallbackGlyph = null; - - font.BuildLookupTable(); - } - - foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) - { - var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); - if ((IntPtr)glyph.NativePtr != IntPtr.Zero) - { - var ptr = font.NativePtr; - ptr->FallbackChar = fallbackCharCandidate; - ptr->FallbackGlyph = glyph.NativePtr; - ptr->FallbackHotData = - (ImFontGlyphHotData*)ptr->IndexedHotData.Address( - fallbackCharCandidate); - break; - } - } - - font.AdjustGlyphMetrics(1 / toolkitPostBuild.Scale, toolkitPostBuild.Scale); + plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths); + plan.PostProcessFullRangeFont(); + plan.CopyGlyphsToRanges(); } catch (Exception e) { @@ -567,32 +378,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal this.fonts[style] = default; } } - - foreach (var (source, targets) in this.fontCopyTargets) - { - foreach (var target in targets) - { - if (target.Ranges is null) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(source, target.Font, missingOnly: true); - } - else - { - for (var i = 0; i < target.Ranges.Length; i += 2) - { - if (target.Ranges[i] == 0) - break; - ImGuiHelpers.CopyGlyphsAcrossFonts( - source, - target.Font, - true, - true, - target.Ranges[i], - target.Ranges[i + 1]); - } - } - } - } } /// @@ -601,103 +386,401 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal // Irrelevant } - /// - /// Creates a relevant for the given . - /// - /// The game font style. - /// The toolkitPostBuild. - /// Min range. - /// Max range. - /// Add extra language glyphs. - /// The font. - private ImFontPtr CreateFontPrivate( - GameFontStyle style, - IFontAtlasBuildToolkitPreBuild toolkitPreBuild, - char minRange, - char maxRange, - bool addExtraLanguageGlyphs) - { - var font = toolkitPreBuild.IgnoreGlobalScale(this.CreateTemplateFont(style, toolkitPreBuild)); - - if (addExtraLanguageGlyphs) - { - var cfg = toolkitPreBuild.FindConfigPtr(font); - toolkitPreBuild.AddExtraGlyphsForDalamudLanguage(new() - { - MergeFont = cfg.DstFont, - SizePx = cfg.SizePixels, - }); - } - - var fas = GameFontStyle.GetRecommendedFamilyAndSize(style.Family, style.SizePt * toolkitPreBuild.Scale); - var horizontalOffset = fas.GetAttribute()?.HorizontalOffset ?? 0; - using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); - ref var fdtFontHeader = ref fdt.FontHeader; - var existing = new SortedSet(); - - if (style is { Bold: false, Italic: false }) - { - if (!this.lateBuildRanges.TryGetValue(font, out var ranges)) - this.lateBuildRanges[font] = ranges = new(); - - ranges.Add((minRange, maxRange)); - } - else - { - if (this.glyphRectIds.TryGetValue(style, out var rectIds)) - existing.UnionWith(rectIds.Keys); - else - rectIds = this.glyphRectIds[style] = new(); - - var glyphs = fdt.Glyphs; - for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) - { - ref var glyph = ref glyphs[fdtGlyphIndex]; - var cint = glyph.CharInt; - if (cint < minRange || cint > maxRange) - continue; - - var c = (char)cint; - if (existing.Contains(c)) - continue; - - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdtFontHeader, glyph); - rectIds[c] = ( - toolkitPreBuild.NewImAtlas.AddCustomRectFontGlyph( - font, - c, - glyph.BoundingWidth + widthAdjustment, - glyph.BoundingHeight, - glyph.AdvanceWidth, - new(horizontalOffset, glyph.CurrentOffsetY)), - fdtGlyphIndex); - } - } - - var scale = toolkitPreBuild.Scale * (style.SizePt / fdt.FontHeader.Size); - foreach (ref var kernPair in fdt.PairAdjustments) - font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset * scale); - - return font; - } - /// /// Creates a new template font. /// - /// The game font style. /// The toolkitPostBuild. + /// The size of the font. /// The font. - private ImFontPtr CreateTemplateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + private ImFontPtr CreateTemplateFont(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, float sizePx) { var font = toolkitPreBuild.AddDalamudAssetFont( DalamudAsset.NotoSansJpMedium, new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, - SizePx = style.SizePx * toolkitPreBuild.Scale, + SizePx = sizePx, }); this.templatedFonts.Add(font); return font; } + + private unsafe void PatchFontMetricsIfNecessary(GameFontStyle style, ImFontPtr font, float atlasScale) + { + if (!this.templatedFonts.Contains(font)) + return; + + var fas = style.Scale(atlasScale).FamilyAndSize; + using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); + ref var fdtFontHeader = ref fdt.FontHeader; + var fontPtr = font.NativePtr; + + var scale = style.SizePt / fdtFontHeader.Size; + fontPtr->Ascent = fdtFontHeader.Ascent * scale; + fontPtr->Descent = fdtFontHeader.Descent * scale; + fontPtr->EllipsisChar = '…'; + } + } + + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "Internal")] + private sealed class FontDrawPlan : IDisposable + { + public readonly GameFontStyle Style; + public readonly GameFontStyle BaseStyle; + public readonly GameFontFamilyAndSizeAttribute BaseAttr; + public readonly int TexCount; + public readonly Dictionary Ranges = new(); + public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new(); + public readonly ushort[] RectLookup = new ushort[0x10000]; + public readonly FdtFileView Fdt; + public readonly ImFontPtr FullRangeFont; + + private readonly IDisposable fdtHandle; + private readonly IGameFontTextureProvider gftp; + + public FontDrawPlan( + GameFontStyle style, + float scale, + IGameFontTextureProvider gameFontTextureProvider, + ImFontPtr fullRangeFont) + { + this.Style = style; + this.BaseStyle = style.Scale(scale); + this.BaseAttr = this.BaseStyle.FamilyAndSize.GetAttribute()!; + this.gftp = gameFontTextureProvider; + this.TexCount = this.gftp.GetFontTextureCount(this.BaseAttr.TexPathFormat); + this.fdtHandle = this.gftp.CreateFdtFileView(this.BaseStyle.FamilyAndSize, out this.Fdt); + this.RectLookup.AsSpan().Fill(ushort.MaxValue); + this.FullRangeFont = fullRangeFont; + this.Ranges[fullRangeFont] = new(0x10000); + } + + public void Dispose() + { + this.fdtHandle.Dispose(); + } + + public void AttachFont(ImFontPtr font, ushort[]? glyphRanges = null) + { + if (!this.Ranges.TryGetValue(font, out var rangeBitArray)) + rangeBitArray = this.Ranges[font] = new(0x10000); + + if (glyphRanges is null) + { + foreach (ref var g in this.Fdt.Glyphs) + { + var c = g.CharInt; + if (c is >= 0x20 and <= 0xFFFE) + rangeBitArray[c] = true; + } + + return; + } + + for (var i = 0; i < glyphRanges.Length - 1; i += 2) + { + if (glyphRanges[i] == 0) + break; + var from = (int)glyphRanges[i]; + var to = (int)glyphRanges[i + 1]; + for (var j = from; j <= to; j++) + rangeBitArray[j] = true; + } + } + + public unsafe void EnsureGlyphs(ImFontAtlasPtr atlas) + { + var glyphs = this.Fdt.Glyphs; + var ranges = this.Ranges[this.FullRangeFont]; + foreach (var (font, extraRange) in this.Ranges) + { + if (font.NativePtr != this.FullRangeFont.NativePtr) + ranges.Or(extraRange); + } + + if (this.Style is not { Weight: 0, SkewStrength: 0 }) + { + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint > char.MaxValue) + continue; + if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) + continue; + + var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(this.Fdt.FontHeader, glyph); + this.RectLookup[cint] = (ushort)this.Rects.Count; + this.Rects.Add( + ( + atlas.AddCustomRectFontGlyph( + this.FullRangeFont, + (char)cint, + glyph.BoundingWidth + widthAdjustment, + glyph.BoundingHeight, + glyph.AdvanceWidth, + new(this.BaseAttr.HorizontalOffset, glyph.CurrentOffsetY)), + fdtGlyphIndex)); + } + } + else + { + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint > char.MaxValue) + continue; + if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) + continue; + + this.RectLookup[cint] = (ushort)this.Rects.Count; + this.Rects.Add((-1, fdtGlyphIndex)); + } + } + } + + public unsafe void PostProcessFullRangeFont() + { + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; + foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan) + { + g.XY *= scale; + g.AdvanceX *= scale; + } + + var pfrf = this.FullRangeFont.NativePtr; + ref var frf = ref *pfrf; + pfrf->FallbackGlyph = null; + ImGuiNative.ImFont_BuildLookupTable(pfrf); + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = ImGuiNative.ImFont_FindGlyphNoFallback(pfrf, fallbackCharCandidate); + if ((nint)glyph == IntPtr.Zero) + continue; + frf.FallbackChar = fallbackCharCandidate; + frf.FallbackGlyph = glyph; + frf.FallbackHotData = + (ImFontGlyphHotData*)frf.IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + + public unsafe void CopyGlyphsToRanges() + { + foreach (var (font, rangeBits) in this.Ranges) + { + if (font.NativePtr == this.FullRangeFont.NativePtr) + continue; + + var lookup = font.IndexLookupWrapped(); + var glyphs = font.GlyphsWrapped(); + foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan) + { + if (!rangeBits[sourceGlyph.Codepoint]) + continue; + + var glyphIndex = ushort.MaxValue; + if (sourceGlyph.Codepoint < lookup.Length) + glyphIndex = lookup[sourceGlyph.Codepoint]; + + if (glyphIndex == ushort.MaxValue) + glyphs.Add(sourceGlyph); + else + glyphs[glyphIndex] = sourceGlyph; + } + + font.NativePtr->FallbackGlyph = null; + font.BuildLookupTable(); + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate).NativePtr; + if ((nint)glyph == IntPtr.Zero) + continue; + + ref var frf = ref *font.NativePtr; + frf.FallbackChar = fallbackCharCandidate; + frf.FallbackGlyph = glyph; + frf.FallbackHotData = + (ImFontGlyphHotData*)frf.IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + } + + public unsafe void SetFullRangeFontGlyphs( + IFontAtlasBuildToolkitPostBuild toolkitPostBuild, + Dictionary allTexFiles, + Dictionary allTextureIndices, + byte*[] pixels8Array, + int[] widths) + { + var glyphs = this.FullRangeFont.GlyphsWrapped(); + var lookups = this.FullRangeFont.IndexLookupWrapped(); + + ref var fdtFontHeader = ref this.Fdt.FontHeader; + var fdtGlyphs = this.Fdt.Glyphs; + var fdtTexSize = new Vector4( + this.Fdt.FontHeader.TextureWidth, + this.Fdt.FontHeader.TextureHeight, + this.Fdt.FontHeader.TextureWidth, + this.Fdt.FontHeader.TextureHeight); + + if (!allTexFiles.TryGetValue(this.BaseAttr.TexPathFormat, out var texFiles)) + { + allTexFiles.Add( + this.BaseAttr.TexPathFormat, + texFiles = ArrayPool.Shared.Rent(this.TexCount)); + } + + if (!allTextureIndices.TryGetValue(this.BaseAttr.TexPathFormat, out var textureIndices)) + { + allTextureIndices.Add( + this.BaseAttr.TexPathFormat, + textureIndices = ArrayPool.Shared.Rent(this.TexCount)); + textureIndices.AsSpan(0, this.TexCount).Fill(-1); + } + + var pixelWidth = Math.Max(1, (int)MathF.Ceiling(this.BaseStyle.Weight + 1)); + var pixelStrength = stackalloc byte[pixelWidth]; + for (var i = 0; i < pixelWidth; i++) + pixelStrength[i] = (byte)(255 * Math.Min(1f, (this.BaseStyle.Weight + 1) - i)); + + var minGlyphY = 0; + var maxGlyphY = 0; + foreach (ref var g in fdtGlyphs) + { + minGlyphY = Math.Min(g.CurrentOffsetY, minGlyphY); + maxGlyphY = Math.Max(g.BoundingHeight + g.CurrentOffsetY, maxGlyphY); + } + + var horzShift = stackalloc int[maxGlyphY - minGlyphY]; + var horzBlend = stackalloc byte[maxGlyphY - minGlyphY]; + horzShift -= minGlyphY; + horzBlend -= minGlyphY; + if (this.BaseStyle.BaseSkewStrength != 0) + { + for (var i = minGlyphY; i < maxGlyphY; i++) + { + float blend = this.BaseStyle.BaseSkewStrength switch + { + > 0 => fdtFontHeader.LineHeight - i, + < 0 => -i, + _ => throw new InvalidOperationException(), + }; + blend *= this.BaseStyle.BaseSkewStrength / fdtFontHeader.LineHeight; + horzShift[i] = (int)MathF.Floor(blend); + horzBlend[i] = (byte)(255 * (blend - horzShift[i])); + } + } + + foreach (var (rectId, fdtGlyphIndex) in this.Rects) + { + ref var fdtGlyph = ref fdtGlyphs[fdtGlyphIndex]; + if (rectId == -1) + { + ref var textureIndex = ref textureIndices[fdtGlyph.TextureIndex]; + if (textureIndex == -1) + { + textureIndex = toolkitPostBuild.StoreTexture( + this.gftp.NewFontTextureRef(this.BaseAttr.TexPathFormat, fdtGlyph.TextureIndex), + true); + } + + var glyph = new ImGuiHelpers.ImFontGlyphReal + { + AdvanceX = fdtGlyph.AdvanceWidth, + Codepoint = fdtGlyph.Char, + Colored = false, + TextureIndex = textureIndex, + Visible = true, + X0 = this.BaseAttr.HorizontalOffset, + Y0 = fdtGlyph.CurrentOffsetY, + U0 = fdtGlyph.TextureOffsetX, + V0 = fdtGlyph.TextureOffsetY, + U1 = fdtGlyph.BoundingWidth, + V1 = fdtGlyph.BoundingHeight, + }; + + glyph.XY1 = glyph.XY0 + glyph.UV1; + glyph.UV1 += glyph.UV0; + glyph.UV /= fdtTexSize; + + glyphs.Add(glyph); + } + else + { + ref var rc = ref *(ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas + .GetCustomRectByIndex(rectId) + .NativePtr; + var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(fdtFontHeader, fdtGlyph); + + // Glyph is scaled at this point; undo that. + ref var glyph = ref glyphs[lookups[rc.GlyphId]]; + glyph.X0 = this.BaseAttr.HorizontalOffset; + glyph.Y0 = fdtGlyph.CurrentOffsetY; + glyph.X1 = glyph.X0 + fdtGlyph.BoundingWidth + widthAdjustment; + glyph.Y1 = glyph.Y0 + fdtGlyph.BoundingHeight; + glyph.AdvanceX = fdtGlyph.AdvanceWidth; + + var pixels8 = pixels8Array[rc.TextureIndex]; + var width = widths[rc.TextureIndex]; + texFiles[fdtGlyph.TextureFileIndex] ??= + this.gftp.GetTexFile(this.BaseAttr.TexPathFormat, fdtGlyph.TextureFileIndex); + var sourceBuffer = texFiles[fdtGlyph.TextureFileIndex].ImageData; + var sourceBufferDelta = fdtGlyph.TextureChannelByteIndex; + + for (var y = 0; y < fdtGlyph.BoundingHeight; y++) + { + var sourcePixelIndex = + ((fdtGlyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + fdtGlyph.TextureOffsetX; + sourcePixelIndex *= 4; + sourcePixelIndex += sourceBufferDelta; + var blend1 = horzBlend[fdtGlyph.CurrentOffsetY + y]; + + var targetOffset = ((rc.Y + y) * width) + rc.X; + for (var x = 0; x < rc.Width; x++) + pixels8[targetOffset + x] = 0; + + targetOffset += horzShift[fdtGlyph.CurrentOffsetY + y]; + if (blend1 == 0) + { + for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) + { + var n = sourceBuffer[sourcePixelIndex + 4]; + for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) + { + ref var p = ref pixels8[targetOffset + boldOffset]; + p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255)); + } + } + } + else + { + var blend2 = 255 - blend1; + for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) + { + var a1 = sourceBuffer[sourcePixelIndex]; + var a2 = x == fdtGlyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourcePixelIndex + 4]; + var n = (a1 * blend1) + (a2 * blend2); + + for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) + { + ref var p = ref pixels8[targetOffset + boldOffset]; + p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255 / 255)); + } + } + } + } + } + } + } } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs index fbfa2d12e..f6c5c6591 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -31,6 +31,13 @@ internal interface IFontHandleSubstance : IDisposable /// /// The toolkit. void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); + + /// + /// Called between and calls.
+ /// Any further modification to will result in undefined behavior. + ///
+ /// The toolkit. + void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); /// /// Called after call. diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs index cd840e5ed..cb7f7c65a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -37,9 +37,13 @@ public struct SafeFontConfig /// /// Config to copy from. public unsafe SafeFontConfig(ImFontConfigPtr config) + : this() { - this.Raw = *config.NativePtr; - this.Raw.GlyphRanges = null; + if (config.NativePtr is not null) + { + this.Raw = *config.NativePtr; + this.Raw.GlyphRanges = null; + } } /// From 2bfddaae168bfacfd29a46d74a728aff96c4cfbc Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 21:21:14 +0900 Subject: [PATCH 17/71] Add minimum range rebuild test --- .../Widgets/GamePrebakedFontsTestWidget.cs | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index 12749114b..dba293e8b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; @@ -24,6 +25,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable private bool useWordWrap; private bool useItalic; private bool useBold; + private bool useMinimumBuild; /// public string[]? CommandShortcuts { get; init; } @@ -80,6 +82,17 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable this.ClearAtlas(); } } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Minimum Range"u8) + { + var v = (byte)(this.useMinimumBuild ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useMinimumBuild = v != 0; + this.ClearAtlas(); + } + } ImGui.SameLine(); if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed) @@ -90,21 +103,6 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable minCapacity: 1024); } - this.privateAtlas ??= - Service.Get().CreateFontAtlas( - nameof(GamePrebakedFontsTestWidget), - FontAtlasAutoRebuildMode.Async, - this.useGlobalScale); - this.fontHandles ??= - Enum.GetValues() - .Where(x => x.GetAttribute() is not null) - .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) - .GroupBy(x => x.Family) - .ToImmutableDictionary( - x => x.Key, - x => x.Select(y => (y, new Lazy(() => this.privateAtlas.NewGameFontHandle(y)))) - .ToArray()); - fixed (byte* labelPtr = "Test Input"u8) { if (ImGuiNative.igInputTextMultiline( @@ -124,9 +122,38 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable this.testStringBuffer.LengthUnsafe = len; this.testStringBuffer.StorageSpan[len] = default; } + + if (this.useMinimumBuild) + _ = this.privateAtlas?.BuildFontsAsync(); } } + this.privateAtlas ??= + Service.Get().CreateFontAtlas( + nameof(GamePrebakedFontsTestWidget), + FontAtlasAutoRebuildMode.Async, + this.useGlobalScale); + this.fontHandles ??= + Enum.GetValues() + .Where(x => x.GetAttribute() is not null) + .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) + .GroupBy(x => x.Family) + .ToImmutableDictionary( + x => x.Key, + x => x.Select( + y => (y, new Lazy( + () => this.useMinimumBuild + ? this.privateAtlas.NewDelegateFontHandle( + e => + e.OnPreBuild( + tk => tk.AddGameGlyphs( + y, + Encoding.UTF8.GetString( + this.testStringBuffer.DataSpan).ToGlyphRange(), + default))) + : this.privateAtlas.NewGameFontHandle(y)))) + .ToArray()); + var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); foreach (var (family, items) in this.fontHandles) { From f172ee2308def7fb1e5567a8505b1b5b1f176db2 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 21:46:10 +0900 Subject: [PATCH 18/71] Better rounding --- .../FontAtlasFactory.BuildToolkit.cs | 2 +- .../Internals/GamePrebakedFontHandle.cs | 91 +++++++++++++++++-- Dalamud/Interface/Utility/ImGuiHelpers.cs | 2 +- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 46fb3f63d..fdef499dd 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -430,7 +430,7 @@ internal sealed partial class FontAtlasFactory foreach (ref var font in this.Fonts.DataSpan) { if (!this.GlobalScaleExclusions.Contains(font)) - font.AdjustGlyphMetrics(1 / scale, scale); + font.AdjustGlyphMetrics(1 / scale, 1 / scale); foreach (var c in FallbackCodepoints) { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 1ac6fdbce..99c817a91 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -369,8 +369,8 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale); plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths); - plan.PostProcessFullRangeFont(); - plan.CopyGlyphsToRanges(); + plan.CopyGlyphsToRanges(toolkitPostBuild); + plan.PostProcessFullRangeFont(toolkitPostBuild.Scale); } catch (Exception e) { @@ -543,17 +543,43 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } - public unsafe void PostProcessFullRangeFont() + public unsafe void PostProcessFullRangeFont(float atlasScale) { + var round = 1 / atlasScale; + var pfrf = this.FullRangeFont.NativePtr; + ref var frf = ref *pfrf; + + frf.FontSize = MathF.Round(frf.FontSize / round) * round; + frf.Ascent = MathF.Round(frf.Ascent / round) * round; + frf.Descent = MathF.Round(frf.Descent / round) * round; + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan) { - g.XY *= scale; - g.AdvanceX *= scale; + var w = (g.X1 - g.X0) * scale; + var h = (g.Y1 - g.Y0) * scale; + g.X0 = MathF.Round((g.X0 * scale) / round) * round; + g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; + g.X1 = g.X0 + w; + g.Y1 = g.Y0 + h; + g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; + } + + var fullRange = this.Ranges[this.FullRangeFont]; + foreach (ref var k in this.Fdt.PairAdjustments) + { + var (leftInt, rightInt) = (k.LeftInt, k.RightInt); + if (leftInt > char.MaxValue || rightInt > char.MaxValue) + continue; + if (!fullRange[leftInt] || !fullRange[rightInt]) + continue; + ImGuiNative.ImFont_AddKerningPair( + pfrf, + (ushort)leftInt, + (ushort)rightInt, + MathF.Round((k.RightOffset * scale) / round) * round); } - var pfrf = this.FullRangeFont.NativePtr; - ref var frf = ref *pfrf; pfrf->FallbackGlyph = null; ImGuiNative.ImFont_BuildLookupTable(pfrf); @@ -571,13 +597,19 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } - public unsafe void CopyGlyphsToRanges() + public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) { + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; + var atlasScale = toolkitPostBuild.Scale; + var round = 1 / atlasScale; + foreach (var (font, rangeBits) in this.Ranges) { if (font.NativePtr == this.FullRangeFont.NativePtr) continue; + var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font); + var lookup = font.IndexLookupWrapped(); var glyphs = font.GlyphsWrapped(); foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan) @@ -590,9 +622,48 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal glyphIndex = lookup[sourceGlyph.Codepoint]; if (glyphIndex == ushort.MaxValue) - glyphs.Add(sourceGlyph); + { + glyphIndex = (ushort)glyphs.Length; + glyphs.Add(default); + } + + ref var g = ref glyphs[glyphIndex]; + g = sourceGlyph; + if (noGlobalScale) + { + g.XY *= scale; + g.AdvanceX *= scale; + } else - glyphs[glyphIndex] = sourceGlyph; + { + var w = (g.X1 - g.X0) * scale; + var h = (g.Y1 - g.Y0) * scale; + g.X0 = MathF.Round((g.X0 * scale) / round) * round; + g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; + g.X1 = g.X0 + w; + g.Y1 = g.Y0 + h; + g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; + } + } + + foreach (ref var k in this.Fdt.PairAdjustments) + { + var (leftInt, rightInt) = (k.LeftInt, k.RightInt); + if (leftInt > char.MaxValue || rightInt > char.MaxValue) + continue; + if (!rangeBits[leftInt] || !rangeBits[rightInt]) + continue; + if (noGlobalScale) + { + font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale); + } + else + { + font.AddKerningPair( + (ushort)leftInt, + (ushort)rightInt, + MathF.Round((k.RightOffset * scale) / round) * round); + } } font.NativePtr->FallbackGlyph = null; diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index ed6ad1dfe..8ba103593 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -202,7 +202,7 @@ public static class ImGuiHelpers /// If a positive number is given, numbers will be rounded to this. public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f) { - Func rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x; + Func rounder = round > 0 ? x => MathF.Round(x / round) * round : x => x; var font = fontPtr.NativePtr; font->FontSize = rounder(font->FontSize * scale); From 015c313c5ebad2cfe70cdac42afa1f833ccb6f1c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 22:02:33 +0900 Subject: [PATCH 19/71] Move UseAxis/Override to FAF --- Dalamud/Interface/Internal/DalamudIme.cs | 7 ++-- .../Interface/Internal/InterfaceManager.cs | 10 ------ .../Windows/Settings/SettingsWindow.cs | 6 ++-- .../Windows/Settings/Tabs/SettingsTabLook.cs | 6 ++-- .../FontAtlasFactory.BuildToolkit.cs | 36 +++++++++++++++++-- .../Internals/FontAtlasFactory.cs | 11 ++++++ Dalamud/Interface/Utility/ImGuiHelpers.cs | 18 +++++----- 7 files changed, 64 insertions(+), 30 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index e030b4e50..28a9075bd 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -11,6 +11,7 @@ using System.Text.Unicode; using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using ImGuiNET; @@ -196,9 +197,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { - if (Service.Get() - .GetFdtReader(GameFontFamilyAndSize.Axis12) - ?.FindGlyph(chr) is null) + if (Service.Get() + ?.GetFdtReader(GameFontFamilyAndSize.Axis12) + .FindGlyph(chr) is null) { if (!this.EncounteredHan) { diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d252321db..3e004727a 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -189,16 +189,6 @@ internal class InterfaceManager : IDisposable, IServiceType /// public bool IsDispatchingEvents { get; set; } = true; - /// - /// Gets or sets a value indicating whether to override configuration for UseAxis. - /// - public bool? UseAxisOverride { get; set; } = null; - - /// - /// Gets a value indicating whether to use AXIS fonts. - /// - public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; - /// /// Gets a value indicating the native handle of the game main window. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 20ffc781c..027e1a571 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -5,6 +5,7 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.Settings.Tabs; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -65,11 +66,12 @@ internal class SettingsWindow : Window { var configuration = Service.Get(); var interfaceManager = Service.Get(); + var fontAtlasFactory = Service.Get(); - var rebuildFont = interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - interfaceManager.UseAxisOverride = null; + fontAtlasFactory.UseAxisOverride = null; if (rebuildFont) interfaceManager.RebuildFonts(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 35f307655..5293e13c4 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -8,6 +8,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -41,9 +42,8 @@ public class SettingsTabLook : SettingsTab (v, c) => c.UseAxisFontsFromGame = v, v => { - var im = Service.Get(); - im.UseAxisOverride = v; - im.RebuildFonts(); + Service.Get().UseAxisOverride = v; + Service.Get().RebuildFonts(); }), new GapSettingsEntry(5, true), diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index fdef499dd..e73ea7548 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -114,7 +114,7 @@ internal sealed partial class FontAtlasFactory return fontPtr; } - /// + /// public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GlobalScaleExclusions.Contains(fontPtr); @@ -275,7 +275,7 @@ internal sealed partial class FontAtlasFactory { ImFontPtr font; glyphRanges ??= this.factory.DefaultGlyphRanges; - if (Service.Get().UseAxis) + if (this.factory.UseAxis) { font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); } @@ -360,7 +360,8 @@ internal sealed partial class FontAtlasFactory public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) { var dalamudConfiguration = Service.Get(); - if (dalamudConfiguration.EffectiveLanguage == "ko") + if (dalamudConfiguration.EffectiveLanguage == "ko" + || Service.GetNullable()?.EncounteredHangul is true) { this.AddDalamudAssetFont( DalamudAsset.NotoSansKrRegular, @@ -374,6 +375,35 @@ internal sealed partial class FontAtlasFactory UnicodeRanges.HangulJamoExtendedB), }); } + + var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc"); + if (!File.Exists(fontPathChs)) + fontPathChs = null; + + var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc"); + if (!File.Exists(fontPathCht)) + fontPathCht = null; + + if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") + { + this.AddFontFromFile(fontPathCht, fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkUnifiedIdeographsExtensionA), + }); + } + else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" + || Service.GetNullable()?.EncounteredHan is true)) + { + this.AddFontFromFile(fontPathChs, fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkUnifiedIdeographsExtensionA), + }); + } } public void PreBuildSubstances() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index fc199ef5a..358ccd845 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; using Dalamud.Interface.GameFonts; @@ -106,6 +107,16 @@ internal sealed partial class FontAtlasFactory }); } + /// + /// Gets or sets a value indicating whether to override configuration for UseAxis. + /// + public bool? UseAxisOverride { get; set; } = null; + + /// + /// Gets a value indicating whether to use AXIS fonts. + /// + public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; + /// /// Gets the service instance of . /// diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 8ba103593..e3b0ff8d1 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -548,6 +548,15 @@ public static class ImGuiHelpers /// The pointer. /// Whether it is empty. public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; + + /// + /// If is default, then returns . + /// + /// The self. + /// The other. + /// if it is not default; otherwise, . + public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => + self.NativePtr is null ? other : self; /// /// Finds the corresponding ImGui viewport ID for the given window handle. @@ -569,15 +578,6 @@ public static class ImGuiHelpers return -1; } - /// - /// If is default, then returns . - /// - /// The self. - /// The other. - /// if it is not default; otherwise, . - public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => - self.NativePtr is null ? other : self; - /// /// Attempts to validate that is valid. /// From 6ccc982d2b1be10283f5c37a7b2e62f05f0f5c78 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 12 Jan 2024 12:21:36 +0900 Subject: [PATCH 20/71] Add docs for RebuildRecommend --- Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs | 13 +++++++++++-- .../Internals/IFontHandleManager.cs | 4 +--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index d32adc1eb..ec3e66e9a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Utility; using ImGuiNET; @@ -18,9 +19,17 @@ public interface IFontAtlas : IDisposable event FontAtlasBuildStepDelegate? BuildStepChange; /// - /// Event fired when a font rebuild operation is suggested.
- /// This will be invoked from the main thread. + /// Event fired when a font rebuild operation is recommended.
+ /// This event will be invoked from the main thread.
+ ///
+ /// Reasons for the event include changes in and + /// initialization of new associated font handles. ///
+ /// + /// You should call or + /// if is not set to true.
+ /// Avoid calling here; it will block the main thread. + ///
event Action? RebuildRecommend; /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs index 795ca61fc..93c688608 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -5,9 +5,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// internal interface IFontHandleManager : IDisposable { - /// - /// Event fired when a font rebuild operation is suggested. - /// + /// event Action? RebuildRecommend; /// From 912cf991dc4741d4ad9ce68a4344c2b6237469a3 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 12 Jan 2024 12:27:28 +0900 Subject: [PATCH 21/71] remove/internalize unused --- Dalamud/Interface/Utility/ImGuiHelpers.cs | 44 ++++++++--------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index e3b0ff8d1..444463d41 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -314,6 +314,7 @@ public static class ImGuiHelpers glyph->U1, glyph->V1, glyph->AdvanceX * scale); + target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint); changed = true; } else if (!missingOnly) @@ -415,6 +416,8 @@ public static class ImGuiHelpers /// If returns null. public static unsafe void* AllocateMemory(int length) { + // TODO: igMemAlloc takes size_t, which is nint; ImGui.NET apparently interpreted that as uint. + // fix that in ImGui.NET. switch (length) { case 0: @@ -436,35 +439,6 @@ public static class ImGuiHelpers } } - /// - /// Mark 4K page as used, after adding a codepoint to a font. - /// - /// The font. - /// The codepoint. - public static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) - { - // Mark 4K page as used - var pageIndex = unchecked((ushort)(codepoint / 4096)); - font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); - } - - /// - /// Creates a new instance of with a natively backed memory. - /// - /// The created instance. - /// Disposable you can call. - public static unsafe IDisposable NewFontAtlasPtrScoped(out ImFontAtlasPtr font) - { - font = new(ImGuiNative.ImFontAtlas_ImFontAtlas()); - var ptr = font.NativePtr; - return Disposable.Create(() => - { - if (ptr != null) - ImGuiNative.ImFontAtlas_destroy(ptr); - ptr = null; - }); - } - /// /// Creates a new instance of with a natively backed memory. /// @@ -557,6 +531,18 @@ public static class ImGuiHelpers /// if it is not default; otherwise, . public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => self.NativePtr is null ? other : self; + + /// + /// Mark 4K page as used, after adding a codepoint to a font. + /// + /// The font. + /// The codepoint. + internal static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) + { + // Mark 4K page as used + var pageIndex = unchecked((ushort)(codepoint / 4096)); + font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); + } /// /// Finds the corresponding ImGui viewport ID for the given window handle. From a1a96d762acf741d248203198b62e043536d461d Mon Sep 17 00:00:00 2001 From: Infi Date: Wed, 17 Jan 2024 19:25:54 +0100 Subject: [PATCH 22/71] Fix swapped U and V check --- Dalamud/Interface/UldWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index 127ea85ec..dd8986bed 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -107,7 +107,7 @@ public class UldWrapper : IDisposable private IDalamudTextureWrap? CopyRect(int width, int height, byte[] rgbaData, UldRoot.PartData part) { - if (part.V + part.W > width || part.U + part.H > height) + if (part.U + part.W > width || part.V + part.H > height) { return null; } From 14c5ad1605ef35e138d000c128f379630841a6f4 Mon Sep 17 00:00:00 2001 From: Kurochi51 Date: Thu, 18 Jan 2024 21:56:12 +0200 Subject: [PATCH 23/71] Expose `CharacterData.ShieldValue` to Dalamud's Character wrapper. (#1608) --- Dalamud/Game/ClientState/Objects/Types/Character.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index a1eb52edc..ac11bcdd0 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -61,6 +61,11 @@ public unsafe class Character : GameObject /// public uint MaxCp => this.Struct->CharacterData.MaxCraftingPoints; + /// + /// Gets the shield percentage of this Chara. + /// + public byte ShieldPercentage => this.Struct->CharacterData.ShieldValue; + /// /// Gets the ClassJob of this Chara. /// From b5696afe94b9ace8c58323a751b5bb88cae9cece Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 18 Jan 2024 21:37:05 +0100 Subject: [PATCH 24/71] Revert "IFontAtlas: font atlas per plugin" --- .../Internal/DalamudConfiguration.cs | 7 +- Dalamud/Interface/GameFonts/FdtFileView.cs | 159 -- .../GameFonts/GameFontFamilyAndSize.cs | 25 +- .../GameFontFamilyAndSizeAttribute.cs | 37 - Dalamud/Interface/GameFonts/GameFontHandle.cs | 85 +- .../Interface/GameFonts/GameFontManager.cs | 507 ++++++ Dalamud/Interface/GameFonts/GameFontStyle.cs | 37 +- Dalamud/Interface/Internal/DalamudIme.cs | 7 +- .../Interface/Internal/DalamudInterface.cs | 13 +- .../Interface/Internal/InterfaceManager.cs | 960 +++++++++--- .../Internal/Windows/ChangelogWindow.cs | 62 +- .../Internal/Windows/Data/DataWindow.cs | 8 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 213 --- .../Windows/Settings/SettingsWindow.cs | 29 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 30 +- .../Windows/Settings/Tabs/SettingsTabLook.cs | 50 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 63 +- .../FontAtlasAutoRebuildMode.cs | 22 - .../ManagedFontAtlas/FontAtlasBuildStep.cs | 38 - .../FontAtlasBuildStepDelegate.cs | 15 - .../FontAtlasBuildToolkitUtilities.cs | 133 -- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 141 -- .../IFontAtlasBuildToolkit.cs | 67 - .../IFontAtlasBuildToolkitPostBuild.cs | 26 - .../IFontAtlasBuildToolkitPostPromotion.cs | 33 - .../IFontAtlasBuildToolkitPreBuild.cs | 186 --- .../Interface/ManagedFontAtlas/IFontHandle.cs | 42 - .../Internals/DelegateFontHandle.cs | 334 ---- .../FontAtlasFactory.BuildToolkit.cs | 682 -------- .../FontAtlasFactory.Implementation.cs | 726 --------- .../Internals/FontAtlasFactory.cs | 368 ----- .../Internals/GamePrebakedFontHandle.cs | 857 ---------- .../Internals/IFontHandleManager.cs | 32 - .../Internals/IFontHandleSubstance.cs | 54 - .../Internals/TrueType.Common.cs | 203 --- .../Internals/TrueType.Enums.cs | 84 - .../Internals/TrueType.Files.cs | 148 -- .../Internals/TrueType.GposGsub.cs | 259 --- .../Internals/TrueType.PointerSpan.cs | 443 ------ .../Internals/TrueType.Tables.cs | 1391 ----------------- .../ManagedFontAtlas/Internals/TrueType.cs | 135 -- .../ManagedFontAtlas/SafeFontConfig.cs | 306 ---- Dalamud/Interface/UiBuilder.cs | 182 +-- Dalamud/Interface/Utility/ImGuiHelpers.cs | 243 +-- 44 files changed, 1499 insertions(+), 7943 deletions(-) delete mode 100644 Dalamud/Interface/GameFonts/FdtFileView.cs delete mode 100644 Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs create mode 100644 Dalamud/Interface/GameFonts/GameFontManager.cs delete mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 66c2745c5..76c8f3603 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -148,9 +148,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable public bool UseAxisFontsFromGame { get; set; } = false; /// - /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use. + /// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness. + /// + /// Before gamma is applied... + /// * ...TTF fonts loaded with stb or FreeType are in linear space. + /// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4. /// - [Obsolete("It happens that nobody touched this setting", true)] public float FontGammaLevel { get; set; } = 1.4f; /// diff --git a/Dalamud/Interface/GameFonts/FdtFileView.cs b/Dalamud/Interface/GameFonts/FdtFileView.cs deleted file mode 100644 index 896a6dbb4..000000000 --- a/Dalamud/Interface/GameFonts/FdtFileView.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System.Collections.Generic; -using System.IO; - -namespace Dalamud.Interface.GameFonts; - -/// -/// Reference member view of a .fdt file data. -/// -internal readonly unsafe struct FdtFileView -{ - private readonly byte* ptr; - - /// - /// Initializes a new instance of the struct. - /// - /// Pointer to the data. - /// Length of the data. - public FdtFileView(void* ptr, int length) - { - this.ptr = (byte*)ptr; - if (length < sizeof(FdtReader.FdtHeader)) - throw new InvalidDataException("Not enough space for a FdtHeader"); - - if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader)) - throw new InvalidDataException("Not enough space for a FontTableHeader"); - if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader) + - (sizeof(FdtReader.FontTableEntry) * this.FontHeader.FontTableEntryCount)) - throw new InvalidDataException("Not enough space for all the FontTableEntry"); - - if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader)) - throw new InvalidDataException("Not enough space for a KerningTableHeader"); - if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader) + - (sizeof(FdtReader.KerningTableEntry) * this.KerningEntryCount)) - throw new InvalidDataException("Not enough space for all the KerningTableEntry"); - } - - /// - /// Gets the file header. - /// - public ref FdtReader.FdtHeader FileHeader => ref *(FdtReader.FdtHeader*)this.ptr; - - /// - /// Gets the font header. - /// - public ref FdtReader.FontTableHeader FontHeader => - ref *(FdtReader.FontTableHeader*)((nint)this.ptr + this.FileHeader.FontTableHeaderOffset); - - /// - /// Gets the glyphs. - /// - public Span Glyphs => new(this.GlyphsUnsafe, this.FontHeader.FontTableEntryCount); - - /// - /// Gets the kerning header. - /// - public ref FdtReader.KerningTableHeader KerningHeader => - ref *(FdtReader.KerningTableHeader*)((nint)this.ptr + this.FileHeader.KerningTableHeaderOffset); - - /// - /// Gets the number of kerning entries. - /// - public int KerningEntryCount => Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); - - /// - /// Gets the kerning entries. - /// - public Span PairAdjustments => new( - this.ptr + this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader), - this.KerningEntryCount); - - /// - /// Gets the maximum texture index. - /// - public int MaxTextureIndex - { - get - { - var i = 0; - foreach (ref var g in this.Glyphs) - { - if (g.TextureIndex > i) - i = g.TextureIndex; - } - - return i; - } - } - - private FdtReader.FontTableEntry* GlyphsUnsafe => - (FdtReader.FontTableEntry*)(this.ptr + this.FileHeader.FontTableHeaderOffset + - sizeof(FdtReader.FontTableHeader)); - - /// - /// Finds the glyph index for the corresponding codepoint. - /// - /// Unicode codepoint (UTF-32 value). - /// Corresponding index, or a negative number according to . - public int FindGlyphIndex(int codepoint) - { - var comp = FdtReader.CodePointToUtf8Int32(codepoint); - - var glyphs = this.GlyphsUnsafe; - var lo = 0; - var hi = this.FontHeader.FontTableEntryCount - 1; - while (lo <= hi) - { - var i = (int)(((uint)hi + (uint)lo) >> 1); - switch (comp.CompareTo(glyphs[i].CharUtf8)) - { - case 0: - return i; - case > 0: - lo = i + 1; - break; - default: - hi = i - 1; - break; - } - } - - return ~lo; - } - - /// - /// Create a glyph range for use with . - /// - /// Merge two ranges into one if distance is below the value specified in this parameter. - /// Glyph ranges. - public ushort[] ToGlyphRanges(int mergeDistance = 8) - { - var glyphs = this.Glyphs; - var ranges = new List(glyphs.Length) - { - checked((ushort)glyphs[0].CharInt), - checked((ushort)glyphs[0].CharInt), - }; - - foreach (ref var glyph in glyphs[1..]) - { - var c32 = glyph.CharInt; - if (c32 >= 0x10000) - break; - - var c16 = unchecked((ushort)c32); - if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) - { - ranges[^1] = c16; - } - else if (ranges[^1] + 1 < c16) - { - ranges.Add(c16); - ranges.Add(c16); - } - } - - ranges.Add(0); - return ranges.ToArray(); - } -} diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs index 6e66cf19b..dd78baf87 100644 --- a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs @@ -3,7 +3,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Enum of available game fonts in specific sizes. /// -public enum GameFontFamilyAndSize +public enum GameFontFamilyAndSize : int { /// /// Placeholder meaning unused. @@ -15,7 +15,6 @@ public enum GameFontFamilyAndSize /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// - [GameFontFamilyAndSize("common/font/AXIS_96.fdt", "common/font/font{0}.tex", -1)] Axis96, /// @@ -23,7 +22,6 @@ public enum GameFontFamilyAndSize /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// - [GameFontFamilyAndSize("common/font/AXIS_12.fdt", "common/font/font{0}.tex", -1)] Axis12, /// @@ -31,7 +29,6 @@ public enum GameFontFamilyAndSize /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// - [GameFontFamilyAndSize("common/font/AXIS_14.fdt", "common/font/font{0}.tex", -1)] Axis14, /// @@ -39,7 +36,6 @@ public enum GameFontFamilyAndSize /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// - [GameFontFamilyAndSize("common/font/AXIS_18.fdt", "common/font/font{0}.tex", -1)] Axis18, /// @@ -47,7 +43,6 @@ public enum GameFontFamilyAndSize /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// - [GameFontFamilyAndSize("common/font/AXIS_36.fdt", "common/font/font{0}.tex", -4)] Axis36, /// @@ -55,7 +50,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// - [GameFontFamilyAndSize("common/font/Jupiter_16.fdt", "common/font/font{0}.tex", -1)] Jupiter16, /// @@ -63,7 +57,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// - [GameFontFamilyAndSize("common/font/Jupiter_20.fdt", "common/font/font{0}.tex", -1)] Jupiter20, /// @@ -71,7 +64,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// - [GameFontFamilyAndSize("common/font/Jupiter_23.fdt", "common/font/font{0}.tex", -1)] Jupiter23, /// @@ -79,7 +71,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// - [GameFontFamilyAndSize("common/font/Jupiter_45.fdt", "common/font/font{0}.tex", -2)] Jupiter45, /// @@ -87,7 +78,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// - [GameFontFamilyAndSize("common/font/Jupiter_46.fdt", "common/font/font{0}.tex", -2)] Jupiter46, /// @@ -95,7 +85,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// - [GameFontFamilyAndSize("common/font/Jupiter_90.fdt", "common/font/font{0}.tex", -4)] Jupiter90, /// @@ -103,7 +92,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// - [GameFontFamilyAndSize("common/font/Meidinger_16.fdt", "common/font/font{0}.tex", -1)] Meidinger16, /// @@ -111,7 +99,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// - [GameFontFamilyAndSize("common/font/Meidinger_20.fdt", "common/font/font{0}.tex", -1)] Meidinger20, /// @@ -119,7 +106,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// - [GameFontFamilyAndSize("common/font/Meidinger_40.fdt", "common/font/font{0}.tex", -4)] Meidinger40, /// @@ -127,7 +113,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_10.fdt", "common/font/font{0}.tex", -1)] MiedingerMid10, /// @@ -135,7 +120,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_12.fdt", "common/font/font{0}.tex", -1)] MiedingerMid12, /// @@ -143,7 +127,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_14.fdt", "common/font/font{0}.tex", -1)] MiedingerMid14, /// @@ -151,7 +134,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_18.fdt", "common/font/font{0}.tex", -1)] MiedingerMid18, /// @@ -159,7 +141,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_36.fdt", "common/font/font{0}.tex", -2)] MiedingerMid36, /// @@ -167,7 +148,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_184.fdt", "common/font/font{0}.tex", -1)] TrumpGothic184, /// @@ -175,7 +155,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_23.fdt", "common/font/font{0}.tex", -1)] TrumpGothic23, /// @@ -183,7 +162,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_34.fdt", "common/font/font{0}.tex", -1)] TrumpGothic34, /// @@ -191,6 +169,5 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_68.fdt", "common/font/font{0}.tex", -3)] TrumpGothic68, } diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs deleted file mode 100644 index f5260e4bc..000000000 --- a/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Dalamud.Interface.GameFonts; - -/// -/// Marks the path for an enum value. -/// -[AttributeUsage(AttributeTargets.Field)] -internal class GameFontFamilyAndSizeAttribute : Attribute -{ - /// - /// Initializes a new instance of the class. - /// - /// Inner path of the file. - /// the file path format for the relevant .tex files. - /// Horizontal offset of the corresponding font. - public GameFontFamilyAndSizeAttribute(string path, string texPathFormat, int horizontalOffset) - { - this.Path = path; - this.TexPathFormat = texPathFormat; - this.HorizontalOffset = horizontalOffset; - } - - /// - /// Gets the path. - /// - public string Path { get; } - - /// - /// Gets the file path format for the relevant .tex files.
- /// Used for (, ). - ///
- public string TexPathFormat { get; } - - /// - /// Gets the horizontal offset of the corresponding font. - /// - public int HorizontalOffset { get; } -} diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 77461aa0a..d71e725c5 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,76 +1,75 @@ +using System; using System.Numerics; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; - using ImGuiNET; namespace Dalamud.Interface.GameFonts; /// -/// ABI-compatible wrapper for . +/// Prepare and keep game font loaded for use in OnDraw. /// -public sealed class GameFontHandle : IFontHandle +public class GameFontHandle : IDisposable { - private readonly IFontHandle.IInternal fontHandle; - private readonly FontAtlasFactory fontAtlasFactory; + private readonly GameFontManager manager; + private readonly GameFontStyle fontStyle; /// /// Initializes a new instance of the class. /// - /// The wrapped . - /// An instance of . - internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory) + /// GameFontManager instance. + /// Font to use. + internal GameFontHandle(GameFontManager manager, GameFontStyle font) { - this.fontHandle = fontHandle; - this.fontAtlasFactory = fontAtlasFactory; + this.manager = manager; + this.fontStyle = font; } - /// - public Exception? LoadException => this.fontHandle.LoadException; - - /// - public bool Available => this.fontHandle.Available; - - /// - [Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)] - public ImFontPtr ImFont => this.fontHandle.ImFont; - /// - /// Gets the font style. Only applicable for . + /// Gets the font style. /// - [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] - public GameFontStyle Style => ((GamePrebakedFontHandle)this.fontHandle).FontStyle; + public GameFontStyle Style => this.fontStyle; /// - /// Gets the relevant .
- ///
- /// Only applicable for game fonts. Otherwise it will throw. + /// Gets a value indicating whether this font is ready for use. ///
- [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] - public FdtReader FdtReader => this.fontAtlasFactory.GetFdtReader(this.Style.FamilyAndSize)!; - - /// - public void Dispose() => this.fontHandle.Dispose(); - - /// - public IDisposable Push() => this.fontHandle.Push(); + public bool Available + { + get + { + unsafe + { + return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null; + } + } + } /// - /// Creates a new .
- ///
- /// Only applicable for game fonts. Otherwise it will throw. + /// Gets the font. + ///
+ public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value; + + /// + /// Gets the FdtReader. + /// + public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); + + /// + /// Creates a new GameFontLayoutPlan.Builder. /// /// Text. /// A new builder for GameFontLayoutPlan. - [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] - public GameFontLayoutPlan.Builder LayoutBuilder(string text) => new(this.ImFont, this.FdtReader, text); + public GameFontLayoutPlan.Builder LayoutBuilder(string text) + { + return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text); + } + + /// + public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); /// /// Draws text. /// /// Text to draw. - [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void Text(string text) { if (!this.Available) @@ -94,7 +93,6 @@ public sealed class GameFontHandle : IFontHandle ///
/// Color. /// Text to draw. - [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextColored(Vector4 col, string text) { ImGui.PushStyleColor(ImGuiCol.Text, col); @@ -106,7 +104,6 @@ public sealed class GameFontHandle : IFontHandle /// Draws disabled text. ///
/// Text to draw. - [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextDisabled(string text) { unsafe diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs new file mode 100644 index 000000000..b3454e085 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -0,0 +1,507 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Utility.Timing; +using ImGuiNET; +using Lumina.Data.Files; +using Serilog; + +using static Dalamud.Interface.Utility.ImGuiHelpers; + +namespace Dalamud.Interface.GameFonts; + +/// +/// Loads game font for use in ImGui. +/// +[ServiceManager.BlockingEarlyLoadedService] +internal class GameFontManager : IServiceType +{ + private static readonly string?[] FontNames = + { + null, + "AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36", + "Jupiter_16", "Jupiter_20", "Jupiter_23", "Jupiter_45", "Jupiter_46", "Jupiter_90", + "Meidinger_16", "Meidinger_20", "Meidinger_40", + "MiedingerMid_10", "MiedingerMid_12", "MiedingerMid_14", "MiedingerMid_18", "MiedingerMid_36", + "TrumpGothic_184", "TrumpGothic_23", "TrumpGothic_34", "TrumpGothic_68", + }; + + private readonly object syncRoot = new(); + + private readonly FdtReader?[] fdts; + private readonly List texturePixels; + private readonly Dictionary fonts = new(); + private readonly Dictionary fontUseCounter = new(); + private readonly Dictionary>> glyphRectIds = new(); + +#pragma warning disable CS0414 + private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; +#pragma warning restore CS0414 + + [ServiceManager.ServiceConstructor] + private GameFontManager(DataManager dataManager) + { + using (Timings.Start("Getting fdt data")) + { + this.fdts = FontNames.Select(fontName => fontName == null ? null : new FdtReader(dataManager.GetFile($"common/font/{fontName}.fdt")!.Data)).ToArray(); + } + + using (Timings.Start("Getting texture data")) + { + var texTasks = Enumerable + .Range(1, 1 + this.fdts + .Where(x => x != null) + .Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max()) + .Max()) + .Select(x => dataManager.GetFile($"common/font/font{x}.tex")!) + .Select(x => new Task(Timings.AttachTimingHandle(() => x.ImageData!))) + .ToArray(); + foreach (var task in texTasks) + task.Start(); + this.texturePixels = texTasks.Select(x => x.GetAwaiter().GetResult()).ToList(); + } + } + + /// + /// Describe font into a string. + /// + /// Font to describe. + /// A string in a form of "FontName (NNNpt)". + public static string DescribeFont(GameFontFamilyAndSize font) + { + return font switch + { + GameFontFamilyAndSize.Undefined => "-", + GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)", + GameFontFamilyAndSize.Axis12 => "AXIS (12pt)", + GameFontFamilyAndSize.Axis14 => "AXIS (14pt)", + GameFontFamilyAndSize.Axis18 => "AXIS (18pt)", + GameFontFamilyAndSize.Axis36 => "AXIS (36pt)", + GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)", + GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)", + GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)", + GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)", + GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)", + GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)", + GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)", + GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)", + GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)", + GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)", + GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)", + GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)", + GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)", + GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)", + GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)", + GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)", + GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)", + GameFontFamilyAndSize.TrumpGothic68 => "Trump Gothic (68pt)", + _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), + }; + } + + /// + /// Determines whether a font should be able to display most of stuff. + /// + /// Font to check. + /// True if it can. + public static bool IsGenericPurposeFont(GameFontFamilyAndSize font) + { + return font switch + { + GameFontFamilyAndSize.Axis96 => true, + GameFontFamilyAndSize.Axis12 => true, + GameFontFamilyAndSize.Axis14 => true, + GameFontFamilyAndSize.Axis18 => true, + GameFontFamilyAndSize.Axis36 => true, + _ => false, + }; + } + + /// + /// Unscales fonts after they have been rendered onto atlas. + /// + /// Font to unscale. + /// Scale factor. + /// Whether to call target.BuildLookupTable(). + public static void UnscaleFont(ImFontPtr fontPtr, float fontScale, bool rebuildLookupTable = true) + { + if (fontScale == 1) + return; + + unsafe + { + var font = fontPtr.NativePtr; + for (int i = 0, i_ = font->IndexedHotData.Size; i < i_; ++i) + { + font->IndexedHotData.Ref(i).AdvanceX /= fontScale; + font->IndexedHotData.Ref(i).OccupiedWidth /= fontScale; + } + + font->FontSize /= fontScale; + font->Ascent /= fontScale; + font->Descent /= fontScale; + if (font->ConfigData != null) + font->ConfigData->SizePixels /= fontScale; + var glyphs = (ImFontGlyphReal*)font->Glyphs.Data; + for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++) + { + var glyph = &glyphs[i]; + glyph->X0 /= fontScale; + glyph->X1 /= fontScale; + glyph->Y0 /= fontScale; + glyph->Y1 /= fontScale; + glyph->AdvanceX /= fontScale; + } + + for (int i = 0, i_ = font->KerningPairs.Size; i < i_; i++) + font->KerningPairs.Ref(i).AdvanceXAdjustment /= fontScale; + for (int i = 0, i_ = font->FrequentKerningPairs.Size; i < i_; i++) + font->FrequentKerningPairs.Ref(i) /= fontScale; + } + + if (rebuildLookupTable && fontPtr.Glyphs.Size > 0) + fontPtr.BuildLookupTableNonstandard(); + } + + /// + /// Create a glyph range for use with ImGui AddFont. + /// + /// Font family and size. + /// Merge two ranges into one if distance is below the value specified in this parameter. + /// Glyph ranges. + public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8) + { + var fdt = this.fdts[(int)family]!; + var ranges = new List(fdt.Glyphs.Count) + { + checked((ushort)fdt.Glyphs[0].CharInt), + checked((ushort)fdt.Glyphs[0].CharInt), + }; + + foreach (var glyph in fdt.Glyphs.Skip(1)) + { + var c32 = glyph.CharInt; + if (c32 >= 0x10000) + break; + + var c16 = unchecked((ushort)c32); + if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) + { + ranges[^1] = c16; + } + else if (ranges[^1] + 1 < c16) + { + ranges.Add(c16); + ranges.Add(c16); + } + } + + return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned); + } + + /// + /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. + /// + /// Font to use. + /// Handle to game font that may or may not be ready yet. + public GameFontHandle NewFontRef(GameFontStyle style) + { + var interfaceManager = Service.Get(); + var needRebuild = false; + + lock (this.syncRoot) + { + this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1; + } + + needRebuild = !this.fonts.ContainsKey(style); + if (needRebuild) + { + Log.Information("[GameFontManager] NewFontRef: Queueing RebuildFonts because {0} has been requested.", style.ToString()); + Service.GetAsync() + .ContinueWith(task => task.Result.RunOnTick(() => interfaceManager.RebuildFonts())); + } + + return new(this, style); + } + + /// + /// Gets the font. + /// + /// Font to get. + /// Corresponding font or null. + public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null); + + /// + /// Gets the corresponding FdtReader. + /// + /// Font to get. + /// Corresponding FdtReader or null. + public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family]; + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) + { + ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable); + } + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) + { + ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable); + } + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) + { + ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); + } + + /// + /// Build fonts before plugins do something more. To be called from InterfaceManager. + /// + public void BuildFonts() + { + this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = true; + + this.glyphRectIds.Clear(); + this.fonts.Clear(); + + lock (this.syncRoot) + { + foreach (var style in this.fontUseCounter.Keys) + this.EnsureFont(style); + } + } + + /// + /// Record that ImGui.GetIO().Fonts.Build() has been called. + /// + public void AfterIoFontsBuild() + { + this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; + } + + /// + /// Checks whether GameFontMamager owns an ImFont. + /// + /// ImFontPtr to check. + /// Whether it owns. + public bool OwnsFont(ImFontPtr fontPtr) => this.fonts.ContainsValue(fontPtr); + + /// + /// Post-build fonts before plugins do something more. To be called from InterfaceManager. + /// + public unsafe void AfterBuildFonts() + { + var interfaceManager = Service.Get(); + var ioFonts = ImGui.GetIO().Fonts; + var fontGamma = interfaceManager.FontGamma; + + var pixels8s = new byte*[ioFonts.Textures.Size]; + var pixels32s = new uint*[ioFonts.Textures.Size]; + var widths = new int[ioFonts.Textures.Size]; + var heights = new int[ioFonts.Textures.Size]; + for (var i = 0; i < pixels8s.Length; i++) + { + ioFonts.GetTexDataAsRGBA32(i, out pixels8s[i], out widths[i], out heights[i]); + pixels32s[i] = (uint*)pixels8s[i]; + } + + foreach (var (style, font) in this.fonts) + { + var fdt = this.fdts[(int)style.FamilyAndSize]; + var scale = style.SizePt / fdt.FontHeader.Size; + var fontPtr = font.NativePtr; + + Log.Verbose("[GameFontManager] AfterBuildFonts: Scaling {0} from {1}pt to {2}pt (scale: {3})", style.ToString(), fdt.FontHeader.Size, style.SizePt, scale); + + fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; + if (fontPtr->ConfigData != null) + fontPtr->ConfigData->SizePixels = fontPtr->FontSize; + fontPtr->Ascent = fdt.FontHeader.Ascent; + fontPtr->Descent = fdt.FontHeader.Descent; + fontPtr->EllipsisChar = '…'; + foreach (var fallbackCharCandidate in "〓?!") + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); + if ((IntPtr)glyph.NativePtr != IntPtr.Zero) + { + var ptr = font.NativePtr; + ptr->FallbackChar = fallbackCharCandidate; + ptr->FallbackGlyph = glyph.NativePtr; + ptr->FallbackHotData = (ImFontGlyphHotData*)ptr->IndexedHotData.Address(fallbackCharCandidate); + break; + } + } + + // I have no idea what's causing NPE, so just to be safe + try + { + if (font.NativePtr != null && font.NativePtr->ConfigData != null) + { + var nameBytes = Encoding.UTF8.GetBytes(style.ToString() + "\0"); + Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); + } + } + catch (NullReferenceException) + { + // do nothing + } + + foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style]) + { + var rc = (ImFontAtlasCustomRectReal*)ioFonts.GetCustomRectByIndex(rectId).NativePtr; + var pixels8 = pixels8s[rc->TextureIndex]; + var pixels32 = pixels32s[rc->TextureIndex]; + var width = widths[rc->TextureIndex]; + var height = heights[rc->TextureIndex]; + var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; + var sourceBufferDelta = glyph.TextureChannelByteIndex; + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); + if (widthAdjustment == 0) + { + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; + pixels32[((rc->Y + y) * width) + rc->X + x] = (uint)(a << 24) | 0xFFFFFFu; + } + } + } + else + { + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) + pixels32[((rc->Y + y) * width) + rc->X + x] = 0xFFFFFFu; + } + + for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++) + { + var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); + for (var y = 0; y < glyph.BoundingHeight; y++) + { + float xDelta = xbold; + if (style.BaseSkewStrength > 0) + xDelta += style.BaseSkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; + else if (style.BaseSkewStrength < 0) + xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; + var xDeltaInt = (int)Math.Floor(xDelta); + var xness = xDelta - xDeltaInt; + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x; + var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; + var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))]; + var n = (a1 * xness) + (a2 * (1 - xness)); + var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; + pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n)); + } + } + } + } + + if (Math.Abs(fontGamma - 1.4f) >= 0.001) + { + // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) + for (int y = rc->Y, y_ = rc->Y + rc->Height; y < y_; y++) + { + for (int x = rc->X, x_ = rc->X + rc->Width; x < x_; x++) + { + var i = (((y * width) + x) * 4) + 3; + pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); + } + } + } + } + + UnscaleFont(font, 1 / scale, false); + } + } + + /// + /// Decrease font reference counter. + /// + /// Font to release. + internal void DecreaseFontRef(GameFontStyle style) + { + lock (this.syncRoot) + { + if (!this.fontUseCounter.ContainsKey(style)) + return; + + if ((this.fontUseCounter[style] -= 1) == 0) + this.fontUseCounter.Remove(style); + } + } + + private unsafe void EnsureFont(GameFontStyle style) + { + var rectIds = this.glyphRectIds[style] = new(); + + var fdt = this.fdts[(int)style.FamilyAndSize]; + if (fdt == null) + return; + + ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); + fontConfig.OversampleH = 1; + fontConfig.OversampleV = 1; + fontConfig.PixelSnapH = false; + + var io = ImGui.GetIO(); + var font = io.Fonts.AddFontDefault(fontConfig); + + fontConfig.Destroy(); + + this.fonts[style] = font; + foreach (var glyph in fdt.Glyphs) + { + var c = glyph.Char; + if (c < 32 || c >= 0xFFFF) + continue; + + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); + rectIds[c] = Tuple.Create( + io.Fonts.AddCustomRectFontGlyph( + font, + c, + glyph.BoundingWidth + widthAdjustment, + glyph.BoundingHeight, + glyph.AdvanceWidth, + new Vector2(0, glyph.CurrentOffsetY)), + glyph); + } + + foreach (var kernPair in fdt.Distances) + font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index fbaf9de07..946473df4 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -64,7 +64,7 @@ public struct GameFontStyle ///
public float SizePt { - readonly get => this.SizePx * 3 / 4; + get => this.SizePx * 3 / 4; set => this.SizePx = value * 4 / 3; } @@ -73,14 +73,14 @@ public struct GameFontStyle ///
public float BaseSkewStrength { - readonly get => this.SkewStrength * this.BaseSizePx / this.SizePx; + get => this.SkewStrength * this.BaseSizePx / this.SizePx; set => this.SkewStrength = value * this.SizePx / this.BaseSizePx; } /// /// Gets the font family. /// - public readonly GameFontFamily Family => this.FamilyAndSize switch + public GameFontFamily Family => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined, GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis, @@ -112,7 +112,7 @@ public struct GameFontStyle /// /// Gets the corresponding GameFontFamilyAndSize but with minimum possible font sizes. /// - public readonly GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch + public GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch { GameFontFamily.Axis => GameFontFamilyAndSize.Axis96, GameFontFamily.Jupiter => GameFontFamilyAndSize.Jupiter16, @@ -126,7 +126,7 @@ public struct GameFontStyle /// /// Gets the base font size in point unit. /// - public readonly float BaseSizePt => this.FamilyAndSize switch + public float BaseSizePt => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => 0, GameFontFamilyAndSize.Axis96 => 9.6f, @@ -158,14 +158,14 @@ public struct GameFontStyle /// /// Gets the base font size in pixel unit. /// - public readonly float BaseSizePx => this.BaseSizePt * 4 / 3; + public float BaseSizePx => this.BaseSizePt * 4 / 3; /// /// Gets or sets a value indicating whether this font is bold. /// public bool Bold { - readonly get => this.Weight > 0f; + get => this.Weight > 0f; set => this.Weight = value ? 1f : 0f; } @@ -174,8 +174,8 @@ public struct GameFontStyle ///
public bool Italic { - readonly get => this.SkewStrength != 0; - set => this.SkewStrength = value ? this.SizePx / 6 : 0; + get => this.SkewStrength != 0; + set => this.SkewStrength = value ? this.SizePx / 7 : 0; } /// @@ -233,26 +233,13 @@ public struct GameFontStyle _ => GameFontFamilyAndSize.Undefined, }; - /// - /// Creates a new scaled instance of struct. - /// - /// The scale. - /// The scaled instance. - public readonly GameFontStyle Scale(float scale) => new() - { - FamilyAndSize = GetRecommendedFamilyAndSize(this.Family, this.SizePt * scale), - SizePx = this.SizePx * scale, - Weight = this.Weight, - SkewStrength = this.SkewStrength * scale, - }; - /// /// Calculates the adjustment to width resulting fron Weight and SkewStrength. /// /// Font header. /// Glyph. /// Width adjustment in pixel unit. - public readonly int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) { var widthDelta = this.Weight; switch (this.BaseSkewStrength) @@ -276,11 +263,11 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public readonly int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// - public override readonly string ToString() + public override string ToString() { return $"GameFontStyle({this.FamilyAndSize}, {this.SizePt}pt, skew={this.SkewStrength}, weight={this.Weight})"; } diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 28a9075bd..e030b4e50 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -11,7 +11,6 @@ using System.Text.Unicode; using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using ImGuiNET; @@ -197,9 +196,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { - if (Service.Get() - ?.GetFdtReader(GameFontFamilyAndSize.Axis12) - .FindGlyph(chr) is null) + if (Service.Get() + .GetFdtReader(GameFontFamilyAndSize.Axis12) + ?.FindGlyph(chr) is null) { if (!this.EncounteredHan) { diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 60c1f9957..95415659b 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -21,7 +21,6 @@ using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -94,8 +93,7 @@ internal class DalamudInterface : IDisposable, IServiceType private DalamudInterface( Dalamud dalamud, DalamudConfiguration configuration, - FontAtlasFactory fontAtlasFactory, - InterfaceManager interfaceManager, + InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, PluginImageCache pluginImageCache, DalamudAssetManager dalamudAssetManager, Game.Framework framework, @@ -105,7 +103,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.dalamud = dalamud; this.configuration = configuration; - this.interfaceManager = interfaceManager; + this.interfaceManager = interfaceManagerWithScene.Manager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -124,14 +122,10 @@ internal class DalamudInterface : IDisposable, IServiceType clientState, configuration, dalamudAssetManager, - fontAtlasFactory, framework, gameGui, titleScreenMenu) { IsOpen = false }; - this.changelogWindow = new ChangelogWindow( - this.titleScreenMenuWindow, - fontAtlasFactory, - dalamudAssetManager) { IsOpen = false }; + this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false }; @@ -213,7 +207,6 @@ internal class DalamudInterface : IDisposable, IServiceType { this.interfaceManager.Draw -= this.OnDraw; - this.WindowSystem.Windows.OfType().AggregateToDisposable().Dispose(); this.WindowSystem.RemoveAllWindows(); this.changelogWindow.Dispose(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 3e004727a..48157fa86 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,10 +1,13 @@ +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Threading.Tasks; +using System.Text; +using System.Text.Unicode; +using System.Threading; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -16,13 +19,10 @@ using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Plugin.Internal; -using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -64,9 +64,11 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private const int NonMainThreadFontAccessWarningCheckInterval = 10000; - private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); - private static long nextNonMainThreadFontAccessWarningCheck; + private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. + private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. + + private readonly HashSet glyphRequests = new(); + private readonly Dictionary loadedFontInfo = new(); private readonly List deferredDisposeTextures = new(); @@ -79,28 +81,28 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); - private readonly SwapChainVtableResolver address = new(); + private readonly ManualResetEvent fontBuildSignal; + private readonly SwapChainVtableResolver address; private readonly Hook setCursorHook; private RawDX11Scene? scene; private Hook? presentHook; private Hook? resizeBuffersHook; - private IFontAtlas? dalamudAtlas; - private IFontHandle.IInternal? defaultFontHandle; - private IFontHandle.IInternal? iconFontHandle; - private IFontHandle.IInternal? monoFontHandle; - // can't access imgui IO before first present call private bool lastWantCapture = false; + private bool isRebuildingFonts = false; private bool isOverrideGameCursor = true; - private IntPtr gameWindowHandle; [ServiceManager.ServiceConstructor] private InterfaceManager() { this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); + + this.fontBuildSignal = new ManualResetEvent(false); + + this.address = new SwapChainVtableResolver(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -115,46 +117,43 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// This event gets called each frame to facilitate ImGui drawing. /// - public event RawDX11Scene.BuildUIDelegate? Draw; + public event RawDX11Scene.BuildUIDelegate Draw; /// /// This event gets called when ResizeBuffers is called. /// - public event Action? ResizeBuffers; + public event Action ResizeBuffers; + + /// + /// Gets or sets an action that is executed right before fonts are rebuilt. + /// + public event Action BuildFonts; /// /// Gets or sets an action that is executed right after fonts are rebuilt. /// - public event Action? AfterBuildFonts; + public event Action AfterBuildFonts; /// - /// Gets the default ImGui font.
- /// Accessing this static property outside of the main thread is dangerous and not supported. + /// Gets the default ImGui font. ///
- public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr DefaultFont { get; private set; } /// - /// Gets an included FontAwesome icon font.
- /// Accessing this static property outside of the main thread is dangerous and not supported. + /// Gets an included FontAwesome icon font. ///
- public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr IconFont { get; private set; } /// - /// Gets an included monospaced font.
- /// Accessing this static property outside of the main thread is dangerous and not supported. + /// Gets an included monospaced font. ///
- public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr MonoFont { get; private set; } /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. /// public ImGuiIOPtr LastImGuiIoPtr { get; set; } - /// - /// Gets the DX11 scene. - /// - public RawDX11Scene? Scene => this.scene; - /// /// Gets the D3D11 device instance. /// @@ -179,6 +178,11 @@ internal class InterfaceManager : IDisposable, IServiceType } } + /// + /// Gets or sets a value indicating whether the fonts are built and ready to use. + /// + public bool FontsReady { get; set; } = false; + /// /// Gets a value indicating whether the Dalamud interface ready to use. /// @@ -190,56 +194,49 @@ internal class InterfaceManager : IDisposable, IServiceType public bool IsDispatchingEvents { get; set; } = true; /// - /// Gets a value indicating the native handle of the game main window. + /// Gets or sets a value indicating whether to override configuration for UseAxis. /// - public IntPtr GameWindowHandle - { - get - { - if (this.gameWindowHandle == 0) - { - nint gwh = 0; - while ((gwh = NativeFunctions.FindWindowEx(0, gwh, "FFXIVGAME", 0)) != 0) - { - _ = User32.GetWindowThreadProcessId(gwh, out var pid); - if (pid == Environment.ProcessId && User32.IsWindowVisible(gwh)) - { - this.gameWindowHandle = gwh; - break; - } - } - } - - return this.gameWindowHandle; - } - } + public bool? UseAxisOverride { get; set; } = null; /// - /// Gets the font build task. + /// Gets a value indicating whether to use AXIS fonts. /// - public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; + public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; + + /// + /// Gets or sets the overrided font gamma value, instead of using the value from configuration. + /// + public float? FontGammaOverride { get; set; } = null; + + /// + /// Gets the font gamma value to use. + /// + public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGammaLevel)); + + /// + /// Gets a value indicating whether we're building fonts but haven't generated atlas yet. + /// + public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0); + + /// + /// Gets a value indicating the native handle of the game main window. + /// + public IntPtr GameWindowHandle { get; private set; } /// /// Dispose of managed and unmanaged resources. /// public void Dispose() { - if (Service.GetNullable() is { } framework) - framework.RunOnFrameworkThread(Disposer).Wait(); - else - Disposer(); - - this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; - this.dalamudAtlas?.Dispose(); - this.scene?.Dispose(); - return; - - void Disposer() + this.framework.RunOnFrameworkThread(() => { this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - } + }).Wait(); + + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + this.scene?.Dispose(); } #nullable enable @@ -379,8 +376,93 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public void RebuildFonts() { + if (this.scene == null) + { + Log.Verbose("[FONT] RebuildFonts(): scene not ready, doing nothing"); + return; + } + Log.Verbose("[FONT] RebuildFonts() called"); - this.dalamudAtlas?.BuildFontsAsync(); + + // don't invoke this multiple times per frame, in case multiple plugins call it + if (!this.isRebuildingFonts) + { + Log.Verbose("[FONT] RebuildFonts() trigger"); + this.isRebuildingFonts = true; + this.scene.OnNewRenderFrame += this.RebuildFontsInternal; + } + } + + /// + /// Wait for the rebuilding fonts to complete. + /// + public void WaitForFontRebuild() + { + this.fontBuildSignal.WaitOne(); + } + + /// + /// Requests a default font of specified size to exist. + /// + /// Font size in pixels. + /// Ranges of glyphs. + /// Requets handle. + public SpecialGlyphRequest NewFontSizeRef(float size, List> ranges) + { + var allContained = false; + var fonts = ImGui.GetIO().Fonts.Fonts; + ImFontPtr foundFont = null; + unsafe + { + for (int i = 0, i_ = fonts.Size; i < i_; i++) + { + if (!this.glyphRequests.Any(x => x.FontInternal.NativePtr == fonts[i].NativePtr)) + continue; + + allContained = true; + foreach (var range in ranges) + { + if (!allContained) + break; + + for (var j = range.Item1; j <= range.Item2 && allContained; j++) + allContained &= fonts[i].FindGlyphNoFallback(j).NativePtr != null; + } + + if (allContained) + foundFont = fonts[i]; + + break; + } + } + + var req = new SpecialGlyphRequest(this, size, ranges); + req.FontInternal = foundFont; + + if (!allContained) + this.RebuildFonts(); + + return req; + } + + /// + /// Requests a default font of specified size to exist. + /// + /// Font size in pixels. + /// Text to calculate glyph ranges from. + /// Requets handle. + public SpecialGlyphRequest NewFontSizeRef(float size, string text) + { + List> ranges = new(); + foreach (var c in new SortedSet(text.ToHashSet())) + { + if (ranges.Any() && ranges[^1].Item2 + 1 == c) + ranges[^1] = Tuple.Create(ranges[^1].Item1, c); + else + ranges.Add(Tuple.Create(c, c)); + } + + return this.NewFontSizeRef(size, ranges); } /// @@ -404,11 +486,11 @@ internal class InterfaceManager : IDisposable, IServiceType try { var dxgiDev = this.Device.QueryInterfaceOrNull(); - var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); + var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); if (dxgiAdapter == null) return null; - var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, MemorySegmentGroup.Local); + var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, SharpDX.DXGI.MemorySegmentGroup.Local); return (memInfo.CurrentUsage, memInfo.CurrentReservation); } catch @@ -434,65 +516,20 @@ internal class InterfaceManager : IDisposable, IServiceType /// Value. internal void SetImmersiveMode(bool enabled) { - if (this.GameWindowHandle == 0) - throw new InvalidOperationException("Game window is not yet ready."); - var value = enabled ? 1 : 0; - ((Result)NativeFunctions.DwmSetWindowAttribute( - this.GameWindowHandle, - NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - ref value, - sizeof(int))).CheckError(); + if (this.GameWindowHandle == nint.Zero) + return; + + int value = enabled ? 1 : 0; + var hr = NativeFunctions.DwmSetWindowAttribute( + this.GameWindowHandle, + NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + ref value, + sizeof(int)); } - private static InterfaceManager WhenFontsReady() + private static void ShowFontError(string path) { - var im = Service.GetNullable(); - if (im?.dalamudAtlas is not { } atlas) - throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call."); - - if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) - { - nextNonMainThreadFontAccessWarningCheck = - Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; - var stack = new StackTrace(); - if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) - { - if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) - { - NonMainThreadFontAccessWarning.Add(plugin, new()); - Log.Warning( - "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", - plugin.Name, - stack); - } - } - else - { - // Dalamud internal should be made safe right now - throw new InvalidOperationException("Attempted to access fonts outside the main thread."); - } - } - - if (!atlas.HasBuiltAtlas) - atlas.BuildTask.GetAwaiter().GetResult(); - return im; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void RenderImGui(RawDX11Scene scene) - { - var conf = Service.Get(); - - // Process information needed by ImGuiHelpers each frame. - ImGuiHelpers.NewFrame(); - - // Enable viewports if there are no issues. - if (conf.IsDisableViewport || scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) - ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; - else - ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; - - scene.Render(); + Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error"); } private void InitScene(IntPtr swapChain) @@ -509,7 +546,7 @@ internal class InterfaceManager : IDisposable, IServiceType Service.ProvideException(ex); Log.Error(ex, "Could not load ImGui dependencies."); - var res = User32.MessageBox( + var res = PInvoke.User32.MessageBox( IntPtr.Zero, "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?", "Dalamud Error", @@ -541,7 +578,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (iniFileInfo.Length > 1200000) { Log.Warning("dalamudUI.ini was over 1mb, deleting"); - iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName!, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); + iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); iniFileInfo.Delete(); } } @@ -586,6 +623,8 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; + this.SetupFonts(); + if (!configuration.IsDocking) { ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable; @@ -636,34 +675,26 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { - Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); - Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); - if (this.scene != null && swapChain != this.scene.SwapChain.NativePointer) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); if (this.scene == null) this.InitScene(swapChain); - Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); - - if (!this.dalamudAtlas!.HasBuiltAtlas) - return this.presentHook!.Original(swapChain, syncInterval, presentFlags); - if (this.address.IsReshade) { - var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); + var pRes = this.presentHook.Original(swapChain, syncInterval, presentFlags); - RenderImGui(this.scene!); + this.RenderImGui(); this.DisposeTextures(); return pRes; } - RenderImGui(this.scene!); + this.RenderImGui(); this.DisposeTextures(); - return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + return this.presentHook.Original(swapChain, syncInterval, presentFlags); } private void DisposeTextures() @@ -680,73 +711,471 @@ internal class InterfaceManager : IDisposable, IServiceType } } - [ServiceManager.CallWhenServicesReady( - "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] - private void ContinueConstruction( - TargetSigScanner sigScanner, - Framework framework, - FontAtlasFactory fontAtlasFactory) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RenderImGui() { - this.dalamudAtlas = fontAtlasFactory - .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); - using (this.dalamudAtlas.SuppressAutoRebuild()) + // Process information needed by ImGuiHelpers each frame. + ImGuiHelpers.NewFrame(); + + // Check if we can still enable viewports without any issues. + this.CheckViewportState(); + + this.scene.Render(); + } + + private void CheckViewportState() + { + var configuration = Service.Get(); + + if (configuration.IsDisableViewport || this.scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) { - this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); - this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild( - tk => tk.AddFontAwesomeIconFont( - new() - { - SizePx = DefaultFontSizePx, - GlyphMinAdvanceX = DefaultFontSizePx, - GlyphMaxAdvanceX = DefaultFontSizePx, - }))); - this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild( - tk => tk.AddDalamudAssetFont( - DalamudAsset.InconsolataRegular, - new() { SizePx = DefaultFontSizePx }))); - this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( - tk => - { - // Note: the first call of this function is done outside the main thread; this is expected. - // Do not use DefaultFont, IconFont, and MonoFont. - // Use font handles directly. - - // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); - - // Broadcast to auto-rebuilding instances - this.AfterBuildFonts?.Invoke(); - }); + ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; + return; } - // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. - _ = this.dalamudAtlas.BuildFontsAsync(false); + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + } - this.address.Setup(sigScanner); + /// + /// Loads font for use in ImGui text functions. + /// + private unsafe void SetupFonts() + { + using var setupFontsTimings = Timings.Start("IM SetupFonts"); + + var gameFontManager = Service.Get(); + var dalamud = Service.Get(); + var io = ImGui.GetIO(); + var ioFonts = io.Fonts; + + var fontGamma = this.FontGamma; + + this.fontBuildSignal.Reset(); + ioFonts.Clear(); + ioFonts.TexDesiredWidth = 4096; + + Log.Verbose("[FONT] SetupFonts - 1"); + + foreach (var v in this.loadedFontInfo) + v.Value.Dispose(); + + this.loadedFontInfo.Clear(); + + Log.Verbose("[FONT] SetupFonts - 2"); + + ImFontConfigPtr fontConfig = null; + List garbageList = new(); try { - if (Service.Get().WindowIsImmersive) - this.SetImmersiveMode(true); + var dummyRangeHandle = GCHandle.Alloc(new ushort[] { '0', '0', 0 }, GCHandleType.Pinned); + garbageList.Add(dummyRangeHandle); + + fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); + fontConfig.OversampleH = 1; + fontConfig.OversampleV = 1; + + var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Regular.otf"); + if (!File.Exists(fontPathJp)) + fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); + if (!File.Exists(fontPathJp)) + ShowFontError(fontPathJp); + Log.Verbose("[FONT] fontPathJp = {0}", fontPathJp); + + var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); + if (!File.Exists(fontPathKr)) + fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); + if (!File.Exists(fontPathKr)) + fontPathKr = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "malgun.ttf"); + if (!File.Exists(fontPathKr)) + fontPathKr = null; + Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); + + var fontPathChs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msyh.ttc"); + if (!File.Exists(fontPathChs)) + fontPathChs = null; + Log.Verbose("[FONT] fontPathChs = {0}", fontPathChs); + + var fontPathCht = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msjh.ttc"); + if (!File.Exists(fontPathCht)) + fontPathCht = null; + Log.Verbose("[FONT] fontPathChs = {0}", fontPathCht); + + // Default font + Log.Verbose("[FONT] SetupFonts - Default font"); + var fontInfo = new TargetFontModification( + "Default", + this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, + this.UseAxis ? DefaultFontSizePx : DefaultFontSizePx + 1, + io.FontGlobalScale); + Log.Verbose("[FONT] SetupFonts - Default corresponding AXIS size: {0}pt ({1}px)", fontInfo.SourceAxis.Style.BaseSizePt, fontInfo.SourceAxis.Style.BaseSizePx); + fontConfig.SizePixels = fontInfo.TargetSizePx * io.FontGlobalScale; + if (this.UseAxis) + { + fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = false; + DefaultFont = ioFonts.AddFontDefault(fontConfig); + this.loadedFontInfo[DefaultFont] = fontInfo; + } + else + { + var rangeHandle = gameFontManager.ToGlyphRanges(GameFontFamilyAndSize.Axis12); + garbageList.Add(rangeHandle); + + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontConfig.SizePixels, fontConfig); + this.loadedFontInfo[DefaultFont] = fontInfo; + } + + if (fontPathKr != null + && (Service.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul)) + { + fontConfig.MergeMode = true; + fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathKr, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + + if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") + { + fontConfig.MergeMode = true; + var rangeHandle = GCHandle.Alloc(new ushort[] + { + (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), + (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), + 0, + }, GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathCht, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" + || this.dalamudIme.EncounteredHan)) + { + fontConfig.MergeMode = true; + var rangeHandle = GCHandle.Alloc(new ushort[] + { + (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), + (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), + 0, + }, GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathChs, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + + // FontAwesome icon font + Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); + { + var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesomeFreeSolid.otf"); + if (!File.Exists(fontPathIcon)) + ShowFontError(fontPathIcon); + + var iconRangeHandle = GCHandle.Alloc(new ushort[] { 0xE000, 0xF8FF, 0, }, GCHandleType.Pinned); + garbageList.Add(iconRangeHandle); + + fontConfig.GlyphRanges = iconRangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, DefaultFontSizePx * io.FontGlobalScale, fontConfig); + this.loadedFontInfo[IconFont] = new("Icon", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); + } + + // Monospace font + Log.Verbose("[FONT] SetupFonts - Monospace font"); + { + var fontPathMono = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf"); + if (!File.Exists(fontPathMono)) + ShowFontError(fontPathMono); + + fontConfig.GlyphRanges = IntPtr.Zero; + fontConfig.PixelSnapH = true; + MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, DefaultFontSizePx * io.FontGlobalScale, fontConfig); + this.loadedFontInfo[MonoFont] = new("Mono", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); + } + + // Default font but in requested size for requested glyphs + Log.Verbose("[FONT] SetupFonts - Default font but in requested size for requested glyphs"); + { + Dictionary> extraFontRequests = new(); + foreach (var extraFontRequest in this.glyphRequests) + { + if (!extraFontRequests.ContainsKey(extraFontRequest.Size)) + extraFontRequests[extraFontRequest.Size] = new(); + extraFontRequests[extraFontRequest.Size].Add(extraFontRequest); + } + + foreach (var (fontSize, requests) in extraFontRequests) + { + List<(ushort, ushort)> codepointRanges = new(4 + requests.Sum(x => x.CodepointRanges.Count)) + { + new(Fallback1Codepoint, Fallback1Codepoint), + new(Fallback2Codepoint, Fallback2Codepoint), + // ImGui default ellipsis characters + new(0x2026, 0x2026), + new(0x0085, 0x0085), + }; + + foreach (var request in requests) + codepointRanges.AddRange(request.CodepointRanges.Select(x => (From: x.Item1, To: x.Item2))); + + codepointRanges.Sort(); + List flattenedRanges = new(); + foreach (var range in codepointRanges) + { + if (flattenedRanges.Any() && flattenedRanges[^1] >= range.Item1 - 1) + { + flattenedRanges[^1] = Math.Max(flattenedRanges[^1], range.Item2); + } + else + { + flattenedRanges.Add(range.Item1); + flattenedRanges.Add(range.Item2); + } + } + + flattenedRanges.Add(0); + + fontInfo = new( + $"Requested({fontSize}px)", + this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, + fontSize, + io.FontGlobalScale); + if (this.UseAxis) + { + fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); + fontConfig.SizePixels = fontInfo.SourceAxis.Style.BaseSizePx; + fontConfig.PixelSnapH = false; + + var sizedFont = ioFonts.AddFontDefault(fontConfig); + this.loadedFontInfo[sizedFont] = fontInfo; + foreach (var request in requests) + request.FontInternal = sizedFont; + } + else + { + var rangeHandle = GCHandle.Alloc(flattenedRanges.ToArray(), GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.PixelSnapH = true; + + var sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontSize * io.FontGlobalScale, fontConfig, rangeHandle.AddrOfPinnedObject()); + this.loadedFontInfo[sizedFont] = fontInfo; + foreach (var request in requests) + request.FontInternal = sizedFont; + } + } + } + + gameFontManager.BuildFonts(); + + var customFontFirstConfigIndex = ioFonts.ConfigData.Size; + + Log.Verbose("[FONT] Invoke OnBuildFonts"); + this.BuildFonts?.InvokeSafely(); + Log.Verbose("[FONT] OnBuildFonts OK!"); + + for (int i = customFontFirstConfigIndex, i_ = ioFonts.ConfigData.Size; i < i_; i++) + { + var config = ioFonts.ConfigData[i]; + if (gameFontManager.OwnsFont(config.DstFont)) + continue; + + config.OversampleH = 1; + config.OversampleV = 1; + + var name = Encoding.UTF8.GetString((byte*)config.Name.Data, config.Name.Count).TrimEnd('\0'); + if (name.IsNullOrEmpty()) + name = $"{config.SizePixels}px"; + + // ImFont information is reflected only if corresponding ImFontConfig has MergeMode not set. + if (config.MergeMode) + { + if (!this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) + { + Log.Warning("MergeMode specified for {0} but not found in loadedFontInfo. Skipping.", name); + continue; + } + } + else + { + if (this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) + { + Log.Warning("MergeMode not specified for {0} but found in loadedFontInfo. Skipping.", name); + continue; + } + + // While the font will be loaded in the scaled size after FontScale is applied, the font will be treated as having the requested size when used from plugins. + this.loadedFontInfo[config.DstFont.NativePtr] = new($"PlReq({name})", config.SizePixels); + } + + config.SizePixels = config.SizePixels * io.FontGlobalScale; + } + + for (int i = 0, i_ = ioFonts.ConfigData.Size; i < i_; i++) + { + var config = ioFonts.ConfigData[i]; + config.RasterizerGamma *= fontGamma; + } + + Log.Verbose("[FONT] ImGui.IO.Build will be called."); + ioFonts.Build(); + gameFontManager.AfterIoFontsBuild(); + this.ClearStacks(); + Log.Verbose("[FONT] ImGui.IO.Build OK!"); + + gameFontManager.AfterBuildFonts(); + + foreach (var (font, mod) in this.loadedFontInfo) + { + // I have no idea what's causing NPE, so just to be safe + try + { + if (font.NativePtr != null && font.NativePtr->ConfigData != null) + { + var nameBytes = Encoding.UTF8.GetBytes($"{mod.Name}\0"); + Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); + } + } + catch (NullReferenceException) + { + // do nothing + } + + Log.Verbose("[FONT] {0}: Unscale with scale value of {1}", mod.Name, mod.Scale); + GameFontManager.UnscaleFont(font, mod.Scale, false); + + if (mod.Axis == TargetFontModification.AxisMode.Overwrite) + { + Log.Verbose("[FONT] {0}: Overwrite from AXIS of size {1}px (was {2}px)", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); + GameFontManager.UnscaleFont(font, font.FontSize / mod.SourceAxis.ImFont.FontSize, false); + var ascentDiff = mod.SourceAxis.ImFont.Ascent - font.Ascent; + font.Ascent += ascentDiff; + font.Descent = ascentDiff; + font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar; + font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar; + ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); + } + else if (mod.Axis == TargetFontModification.AxisMode.GameGlyphsOnly) + { + Log.Verbose("[FONT] {0}: Overwrite game specific glyphs from AXIS of size {1}px", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); + if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) + mod.SourceAxis.ImFont.FontSize -= 1; + ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); + if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) + mod.SourceAxis.ImFont.FontSize += 1; + } + + Log.Verbose("[FONT] {0}: Resize from {1}px to {2}px", mod.Name, font.FontSize, mod.TargetSizePx); + GameFontManager.UnscaleFont(font, font.FontSize / mod.TargetSizePx, false); + } + + // Fill missing glyphs in MonoFont from DefaultFont + ImGuiHelpers.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); + + for (int i = 0, i_ = ioFonts.Fonts.Size; i < i_; i++) + { + var font = ioFonts.Fonts[i]; + if (font.Glyphs.Size == 0) + { + Log.Warning("[FONT] Font has no glyph: {0}", font.GetDebugName()); + continue; + } + + if (font.FindGlyphNoFallback(Fallback1Codepoint).NativePtr != null) + font.FallbackChar = Fallback1Codepoint; + + font.BuildLookupTableNonstandard(); + } + + Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); + this.AfterBuildFonts?.InvokeSafely(); + Log.Verbose("[FONT] OnAfterBuildFonts OK!"); + + if (ioFonts.Fonts[0].NativePtr != DefaultFont.NativePtr) + Log.Warning("[FONT] First font is not DefaultFont"); + + Log.Verbose("[FONT] Fonts built!"); + + this.fontBuildSignal.Set(); + + this.FontsReady = true; } - catch (Exception ex) + finally { - Log.Error(ex, "Could not enable immersive mode"); + if (fontConfig.NativePtr != null) + fontConfig.Destroy(); + + foreach (var garbage in garbageList) + garbage.Free(); } + } - this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); + [ServiceManager.CallWhenServicesReady( + "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] + private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration) + { + this.address.Setup(sigScanner); + this.framework.RunOnFrameworkThread(() => + { + while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) + { + _ = User32.GetWindowThreadProcessId(this.GameWindowHandle, out var pid); - Log.Verbose("===== S W A P C H A I N ====="); - Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); - Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); + if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle)) + break; + } - this.setCursorHook.Enable(); - this.presentHook.Enable(); - this.resizeBuffersHook.Enable(); + try + { + if (configuration.WindowIsImmersive) + this.SetImmersiveMode(true); + } + catch (Exception ex) + { + Log.Error(ex, "Could not enable immersive mode"); + } + + this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); + this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); + + Log.Verbose("===== S W A P C H A I N ====="); + Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); + Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); + + this.setCursorHook.Enable(); + this.presentHook.Enable(); + this.resizeBuffersHook.Enable(); + }); + } + + // This is intended to only be called as a handler attached to scene.OnNewRenderFrame + private void RebuildFontsInternal() + { + Log.Verbose("[FONT] RebuildFontsInternal() called"); + this.SetupFonts(); + + Log.Verbose("[FONT] RebuildFontsInternal() detaching"); + this.scene!.OnNewRenderFrame -= this.RebuildFontsInternal; + + Log.Verbose("[FONT] Calling InvalidateFonts"); + this.scene.InvalidateFonts(); + + Log.Verbose("[FONT] Font Rebuild OK!"); + + this.isRebuildingFonts = false; } private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) @@ -777,17 +1206,14 @@ internal class InterfaceManager : IDisposable, IServiceType private IntPtr SetCursorDetour(IntPtr hCursor) { - if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) + if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed - ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() - : this.setCursorHook.Original(hCursor); + return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); } private void OnNewInputFrame() { - var io = ImGui.GetIO(); var dalamudInterface = Service.GetNullable(); var gamepadState = Service.GetNullable(); var keyState = Service.GetNullable(); @@ -795,21 +1221,18 @@ internal class InterfaceManager : IDisposable, IServiceType if (dalamudInterface == null || gamepadState == null || keyState == null) return; - // Prevent setting the footgun from ImGui Demo; the Space key isn't removing the flag at the moment. - io.ConfigFlags &= ~ImGuiConfigFlags.NoMouse; - // fix for keys in game getting stuck, if you were holding a game key (like run) // and then clicked on an imgui textbox - imgui would swallow the keyup event, // so the game would think the key remained pressed continuously until you left // imgui and pressed and released the key again - if (io.WantTextInput) + if (ImGui.GetIO().WantTextInput) { keyState.ClearAll(); } // TODO: mouse state? - var gamepadEnabled = (io.BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; + var gamepadEnabled = (ImGui.GetIO().BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; // NOTE (Chiv) Activate ImGui navigation via L1+L3 press // (mimicking how mouse navigation is activated via L1+R3 press in game). @@ -817,12 +1240,12 @@ internal class InterfaceManager : IDisposable, IServiceType && gamepadState.Raw(GamepadButtons.L1) > 0 && gamepadState.Pressed(GamepadButtons.L3) > 0) { - io.ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; + ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; gamepadState.NavEnableGamepad ^= true; dalamudInterface.ToggleGamepadModeNotifierWindow(); } - if (gamepadEnabled && (io.ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) + if (gamepadEnabled && (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) { var northButton = gamepadState.Raw(GamepadButtons.North) != 0; var eastButton = gamepadState.Raw(GamepadButtons.East) != 0; @@ -841,6 +1264,7 @@ internal class InterfaceManager : IDisposable, IServiceType var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0; var r2Button = gamepadState.Raw(GamepadButtons.R2) != 0; + var io = ImGui.GetIO(); io.AddKeyEvent(ImGuiKey.GamepadFaceUp, northButton); io.AddKeyEvent(ImGuiKey.GamepadFaceRight, eastButton); io.AddKeyEvent(ImGuiKey.GamepadFaceDown, southButton); @@ -888,10 +1312,7 @@ internal class InterfaceManager : IDisposable, IServiceType var snap = ImGuiManagedAsserts.GetSnapshot(); if (this.IsDispatchingEvents) - { - using (this.defaultFontHandle?.Push()) - this.Draw?.Invoke(); - } + this.Draw?.Invoke(); ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); @@ -918,4 +1339,123 @@ internal class InterfaceManager : IDisposable, IServiceType /// public InterfaceManager Manager { get; init; } } + + /// + /// Represents a glyph request. + /// + public class SpecialGlyphRequest : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// InterfaceManager to associate. + /// Font size in pixels. + /// Codepoint ranges. + internal SpecialGlyphRequest(InterfaceManager manager, float size, List> ranges) + { + this.Manager = manager; + this.Size = size; + this.CodepointRanges = ranges; + this.Manager.glyphRequests.Add(this); + } + + /// + /// Gets the font of specified size, or DefaultFont if it's not ready yet. + /// + public ImFontPtr Font + { + get + { + unsafe + { + return this.FontInternal.NativePtr == null ? DefaultFont : this.FontInternal; + } + } + } + + /// + /// Gets or sets the associated ImFont. + /// + internal ImFontPtr FontInternal { get; set; } + + /// + /// Gets associated InterfaceManager. + /// + internal InterfaceManager Manager { get; init; } + + /// + /// Gets font size. + /// + internal float Size { get; init; } + + /// + /// Gets codepoint ranges. + /// + internal List> CodepointRanges { get; init; } + + /// + public void Dispose() + { + this.Manager.glyphRequests.Remove(this); + } + } + + private unsafe class TargetFontModification : IDisposable + { + /// + /// Initializes a new instance of the class. + /// Constructs new target font modification information, assuming that AXIS fonts will not be applied. + /// + /// Name of the font to write to ImGui font information. + /// Target font size in pixels, which will not be considered for further scaling. + internal TargetFontModification(string name, float sizePx) + { + this.Name = name; + this.Axis = AxisMode.Suppress; + this.TargetSizePx = sizePx; + this.Scale = 1; + this.SourceAxis = null; + } + + /// + /// Initializes a new instance of the class. + /// Constructs new target font modification information. + /// + /// Name of the font to write to ImGui font information. + /// Whether and how to use AXIS fonts. + /// Target font size in pixels, which will not be considered for further scaling. + /// Font scale to be referred for loading AXIS font of appropriate size. + internal TargetFontModification(string name, AxisMode axis, float sizePx, float globalFontScale) + { + this.Name = name; + this.Axis = axis; + this.TargetSizePx = sizePx; + this.Scale = globalFontScale; + this.SourceAxis = Service.Get().NewFontRef(new(GameFontFamily.Axis, this.TargetSizePx * this.Scale)); + } + + internal enum AxisMode + { + Suppress, + GameGlyphsOnly, + Overwrite, + } + + internal string Name { get; private init; } + + internal AxisMode Axis { get; private init; } + + internal float TargetSizePx { get; private init; } + + internal float Scale { get; private init; } + + internal GameFontHandle? SourceAxis { get; private init; } + + internal bool SourceAxisAvailable => this.SourceAxis != null && this.SourceAxis.ImFont.NativePtr != null; + + public void Dispose() + { + this.SourceAxis?.Dispose(); + } + } } diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index ae59db36a..b9e7ab686 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Linq; using System.Numerics; @@ -6,8 +7,6 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -32,14 +31,8 @@ internal sealed class ChangelogWindow : Window, IDisposable • Plugins can now add tooltips and interaction to the server info bar • The Dalamud/plugin installer UI has been refreshed "; - + private readonly TitleScreenMenuWindow tsmWindow; - - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private readonly IFontAtlas privateAtlas; - private readonly Lazy bannerFont; - private readonly Lazy apiBumpExplainerTexture; - private readonly Lazy logoTexture; private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f)) { @@ -53,36 +46,27 @@ internal sealed class ChangelogWindow : Window, IDisposable Point2 = Vector2.One, }; + private IDalamudTextureWrap? apiBumpExplainerTexture; + private IDalamudTextureWrap? logoTexture; + private GameFontHandle? bannerFont; + private State state = State.WindowFadeIn; private bool needFadeRestart = false; - + /// /// Initializes a new instance of the class. /// /// TSM window. - /// An instance of . - /// An instance of . - public ChangelogWindow( - TitleScreenMenuWindow tsmWindow, - FontAtlasFactory fontAtlasFactory, - DalamudAssetManager assets) + public ChangelogWindow(TitleScreenMenuWindow tsmWindow) : base("What's new in Dalamud?##ChangelogWindow", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse, true) { this.tsmWindow = tsmWindow; this.Namespace = "DalamudChangelogWindow"; - this.privateAtlas = this.scopedFinalizer.Add( - fontAtlasFactory.CreateFontAtlas(this.Namespace, FontAtlasAutoRebuildMode.Async)); - this.bannerFont = new( - () => this.scopedFinalizer.Add( - this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.MiedingerMid18)))); - - this.apiBumpExplainerTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.ChangelogApiBumpIcon)); - this.logoTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.Logo)); // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - _ = this.bannerFont.Value; + Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); } private enum State @@ -113,12 +97,20 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - _ = this.bannerFont; + this.MakeFont(Service.Get()); this.state = State.WindowFadeIn; this.windowFade.Reset(); this.bodyFade.Reset(); this.needFadeRestart = true; + + if (this.apiBumpExplainerTexture == null) + { + var dalamud = Service.Get(); + var tm = Service.Get(); + this.apiBumpExplainerTexture = tm.GetTextureFromFile(new FileInfo(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "changelogApiBump.png"))) + ?? throw new Exception("Could not load api bump explainer."); + } base.OnOpen(); } @@ -194,7 +186,10 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.SetCursorPos(new Vector2(logoContainerSize.X / 2 - logoSize.X / 2, logoContainerSize.Y / 2 - logoSize.Y / 2)); using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f))) - ImGui.Image(this.logoTexture.Value.ImGuiHandle, logoSize); + { + this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); + ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); + } } ImGui.SameLine(); @@ -210,7 +205,7 @@ internal sealed class ChangelogWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f))) { - using var font = this.bannerFont.Value.Push(); + using var font = ImRaii.PushFont(this.bannerFont!.ImFont); switch (this.state) { @@ -280,11 +275,9 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.TextWrapped("If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available."); ImGuiHelpers.ScaledDummy(15); - - ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture.Value.Width); - ImGui.Image( - this.apiBumpExplainerTexture.Value.ImGuiHandle, - this.apiBumpExplainerTexture.Value.Size); + + ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width); + ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size); DrawNextButton(State.Links); break; @@ -384,4 +377,7 @@ internal sealed class ChangelogWindow : Window, IDisposable public void Dispose() { } + + private void MakeFont(GameFontManager gfm) => + this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); } diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 951d3d91c..20c3d6d01 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -6,8 +6,6 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Utility; - using ImGuiNET; using Serilog; @@ -16,7 +14,7 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class responsible for drawing the data/debug window. /// -internal class DataWindow : Window, IDisposable +internal class DataWindow : Window { private readonly IDataWindowWidget[] modules = { @@ -36,7 +34,6 @@ internal class DataWindow : Window, IDisposable new FlyTextWidget(), new FontAwesomeTestWidget(), new GameInventoryTestWidget(), - new GamePrebakedFontsTestWidget(), new GamepadWidget(), new GaugeWidget(), new HookWidget(), @@ -79,9 +76,6 @@ internal class DataWindow : Window, IDisposable this.Load(); } - /// - public void Dispose() => this.modules.OfType().AggregateToDisposable().Dispose(); - /// public override void OnOpen() { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs deleted file mode 100644 index dba293e8b..000000000 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; - -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; -using Dalamud.Interface.Utility; -using Dalamud.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.Internal.Windows.Data.Widgets; - -/// -/// Widget for testing game prebaked fonts. -/// -internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable -{ - private ImVectorWrapper testStringBuffer; - private IFontAtlas? privateAtlas; - private IReadOnlyDictionary Handle)[]>? fontHandles; - private bool useGlobalScale; - private bool useWordWrap; - private bool useItalic; - private bool useBold; - private bool useMinimumBuild; - - /// - public string[]? CommandShortcuts { get; init; } - - /// - public string DisplayName { get; init; } = "Game Prebaked Fonts"; - - /// - public bool Ready { get; set; } - - /// - public void Load() => this.Ready = true; - - /// - public unsafe void Draw() - { - ImGui.AlignTextToFramePadding(); - fixed (byte* labelPtr = "Global Scale"u8) - { - var v = (byte)(this.useGlobalScale ? 1 : 0); - if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) - { - this.useGlobalScale = v != 0; - this.ClearAtlas(); - } - } - - ImGui.SameLine(); - fixed (byte* labelPtr = "Word Wrap"u8) - { - var v = (byte)(this.useWordWrap ? 1 : 0); - if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) - this.useWordWrap = v != 0; - } - - ImGui.SameLine(); - fixed (byte* labelPtr = "Italic"u8) - { - var v = (byte)(this.useItalic ? 1 : 0); - if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) - { - this.useItalic = v != 0; - this.ClearAtlas(); - } - } - - ImGui.SameLine(); - fixed (byte* labelPtr = "Bold"u8) - { - var v = (byte)(this.useBold ? 1 : 0); - if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) - { - this.useBold = v != 0; - this.ClearAtlas(); - } - } - - ImGui.SameLine(); - fixed (byte* labelPtr = "Minimum Range"u8) - { - var v = (byte)(this.useMinimumBuild ? 1 : 0); - if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) - { - this.useMinimumBuild = v != 0; - this.ClearAtlas(); - } - } - - ImGui.SameLine(); - if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed) - { - this.testStringBuffer.Dispose(); - this.testStringBuffer = ImVectorWrapper.CreateFromSpan( - "(Game)-[Font] {Test}. 0123456789!! <氣気气きキ기>。"u8, - minCapacity: 1024); - } - - fixed (byte* labelPtr = "Test Input"u8) - { - if (ImGuiNative.igInputTextMultiline( - labelPtr, - this.testStringBuffer.Data, - (uint)this.testStringBuffer.Capacity, - new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale), - 0, - null, - null) != 0) - { - var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); - if (len + 4 >= this.testStringBuffer.Capacity) - this.testStringBuffer.EnsureCapacityExponential(len + 4); - if (len < this.testStringBuffer.Capacity) - { - this.testStringBuffer.LengthUnsafe = len; - this.testStringBuffer.StorageSpan[len] = default; - } - - if (this.useMinimumBuild) - _ = this.privateAtlas?.BuildFontsAsync(); - } - } - - this.privateAtlas ??= - Service.Get().CreateFontAtlas( - nameof(GamePrebakedFontsTestWidget), - FontAtlasAutoRebuildMode.Async, - this.useGlobalScale); - this.fontHandles ??= - Enum.GetValues() - .Where(x => x.GetAttribute() is not null) - .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) - .GroupBy(x => x.Family) - .ToImmutableDictionary( - x => x.Key, - x => x.Select( - y => (y, new Lazy( - () => this.useMinimumBuild - ? this.privateAtlas.NewDelegateFontHandle( - e => - e.OnPreBuild( - tk => tk.AddGameGlyphs( - y, - Encoding.UTF8.GetString( - this.testStringBuffer.DataSpan).ToGlyphRange(), - default))) - : this.privateAtlas.NewGameFontHandle(y)))) - .ToArray()); - - var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); - foreach (var (family, items) in this.fontHandles) - { - if (!ImGui.CollapsingHeader($"{family} Family")) - continue; - - foreach (var (gfs, handle) in items) - { - ImGui.TextUnformatted($"{gfs.SizePt}pt"); - ImGui.SameLine(offsetX); - ImGuiNative.igPushTextWrapPos(this.useWordWrap ? 0f : -1f); - try - { - if (handle.Value.LoadException is { } exc) - { - ImGui.TextUnformatted(exc.ToString()); - } - else if (!handle.Value.Available) - { - fixed (byte* labelPtr = "Loading..."u8) - ImGuiNative.igTextUnformatted(labelPtr, labelPtr + 8 + ((Environment.TickCount / 200) % 3)); - } - else - { - if (!this.useGlobalScale) - ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); - using var pushPop = handle.Value.Push(); - ImGuiNative.igTextUnformatted( - this.testStringBuffer.Data, - this.testStringBuffer.Data + this.testStringBuffer.Length); - } - } - finally - { - ImGuiNative.igPopTextWrapPos(); - ImGuiNative.igSetWindowFontScale(1); - } - } - } - } - - /// - public void Dispose() - { - this.ClearAtlas(); - this.testStringBuffer.Dispose(); - } - - private void ClearAtlas() - { - this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value)) - .AggregateToDisposable().Dispose(); - this.fontHandles = null; - this.privateAtlas?.Dispose(); - this.privateAtlas = null; - } -} diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 027e1a571..7d4489f8d 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -5,10 +5,10 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.Settings.Tabs; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Internal; using Dalamud.Utility; using ImGuiNET; @@ -19,7 +19,14 @@ namespace Dalamud.Interface.Internal.Windows.Settings; /// internal class SettingsWindow : Window { - private SettingsTab[]? tabs; + private readonly SettingsTab[] tabs = + { + new SettingsTabGeneral(), + new SettingsTabLook(), + new SettingsTabDtr(), + new SettingsTabExperimental(), + new SettingsTabAbout(), + }; private string searchInput = string.Empty; @@ -42,15 +49,6 @@ internal class SettingsWindow : Window /// public override void OnOpen() { - this.tabs ??= new SettingsTab[] - { - new SettingsTabGeneral(), - new SettingsTabLook(), - new SettingsTabDtr(), - new SettingsTabExperimental(), - new SettingsTabAbout(), - }; - foreach (var settingsTab in this.tabs) { settingsTab.Load(); @@ -66,12 +64,15 @@ internal class SettingsWindow : Window { var configuration = Service.Get(); var interfaceManager = Service.Get(); - var fontAtlasFactory = Service.Get(); - var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = + ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale || + interfaceManager.FontGamma != configuration.FontGammaLevel || + interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - fontAtlasFactory.UseAxisOverride = null; + interfaceManager.FontGammaOverride = null; + interfaceManager.UseAxisOverride = null; if (rebuildFont) interfaceManager.RebuildFonts(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 8714fd666..5b6f6b02f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -1,13 +1,13 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Numerics; using CheapLoc; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; @@ -15,6 +15,7 @@ using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; +using ImGuiScene; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; @@ -172,21 +173,16 @@ Contribute at: https://github.com/goatcorp/Dalamud "; private readonly Stopwatch creditsThrottler; - private readonly IFontAtlas privateAtlas; private string creditsText; private bool resetNow = false; private IDalamudTextureWrap? logoTexture; - private IFontHandle? thankYouFont; + private GameFontHandle? thankYouFont; public SettingsTabAbout() { this.creditsThrottler = new(); - - this.privateAtlas = Service - .Get() - .CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async); } public override SettingsEntry[] Entries { get; } = { }; @@ -211,7 +207,11 @@ Contribute at: https://github.com/goatcorp/Dalamud this.creditsThrottler.Restart(); - this.thankYouFont ??= this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.TrumpGothic34)); + if (this.thankYouFont == null) + { + var gfm = Service.Get(); + this.thankYouFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.TrumpGothic34)); + } this.resetNow = true; @@ -269,12 +269,14 @@ Contribute at: https://github.com/goatcorp/Dalamud if (this.thankYouFont != null) { - using var fontPush = this.thankYouFont.Push(); + ImGui.PushFont(this.thankYouFont.ImFont); var thankYouLenX = ImGui.CalcTextSize(ThankYouText).X; ImGui.Dummy(new Vector2((windowX / 2) - (thankYouLenX / 2), 0f)); ImGui.SameLine(); ImGui.TextUnformatted(ThankYouText); + + ImGui.PopFont(); } ImGuiHelpers.ScaledDummy(0, windowSize.Y + 50f); @@ -303,5 +305,9 @@ Contribute at: https://github.com/goatcorp/Dalamud /// /// Disposes of managed and unmanaged resources. /// - public override void Dispose() => this.privateAtlas.Dispose(); + public override void Dispose() + { + this.logoTexture?.Dispose(); + this.thankYouFont?.Dispose(); + } } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 5293e13c4..02e8ce789 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,14 +1,12 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; -using System.Text; using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -30,6 +28,7 @@ public class SettingsTabLook : SettingsTab }; private float globalUiScale; + private float fontGamma; public override SettingsEntry[] Entries { get; } = { @@ -42,8 +41,9 @@ public class SettingsTabLook : SettingsTab (v, c) => c.UseAxisFontsFromGame = v, v => { - Service.Get().UseAxisOverride = v; - Service.Get().RebuildFonts(); + var im = Service.Get(); + im.UseAxisOverride = v; + im.RebuildFonts(); }), new GapSettingsEntry(5, true), @@ -145,7 +145,6 @@ public class SettingsTabLook : SettingsTab public override void Draw() { var interfaceManager = Service.Get(); - var fontBuildTask = interfaceManager.FontBuildTask; ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); @@ -165,19 +164,6 @@ public class SettingsTabLook : SettingsTab } } - if (!fontBuildTask.IsCompleted) - { - ImGui.SameLine(); - var buildingFonts = Loc.Localize("DalamudSettingsFontBuildInProgressWithEndingThreeDots", "Building fonts..."); - unsafe - { - var len = Encoding.UTF8.GetByteCount(buildingFonts); - var p = stackalloc byte[len]; - Encoding.UTF8.GetBytes(buildingFonts, new(p, len)); - ImGuiNative.igTextUnformatted(p, (p + len + ((Environment.TickCount / 200) % 3)) - 2); - } - } - var globalUiScaleInPt = 12f * this.globalUiScale; if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp)) { @@ -188,25 +174,33 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale text in all XIVLauncher UI elements - this is useful for 4K displays.")); - if (fontBuildTask.IsFaulted || fontBuildTask.IsCanceled) + ImGuiHelpers.ScaledDummy(5); + + ImGui.AlignTextToFramePadding(); + ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) { - ImGui.TextColored( - ImGuiColors.DalamudRed, - Loc.Localize("DalamudSettingsFontBuildFaulted", "Failed to load fonts as requested.")); - if (fontBuildTask.Exception is not null - && ImGui.CollapsingHeader("##DalamudSetingsFontBuildFaultReason")) - { - foreach (var e in fontBuildTask.Exception.InnerExceptions) - ImGui.TextUnformatted(e.ToString()); - } + this.fontGamma = 1.4f; + interfaceManager.FontGammaOverride = this.fontGamma; + interfaceManager.RebuildFonts(); } + if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp)) + { + interfaceManager.FontGammaOverride = this.fontGamma; + interfaceManager.RebuildFonts(); + } + + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFontGammaHint", "Changes the thickness of text.")); + base.Draw(); } public override void Load() { this.globalUiScale = Service.Get().GlobalUiScale; + this.fontGamma = Service.Get().FontGammaLevel; base.Load(); } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 9c385a99c..42bca89ff 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -7,14 +7,11 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; -using Dalamud.Utility; using ImGuiNET; @@ -30,17 +27,16 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly ClientState clientState; private readonly DalamudConfiguration configuration; + private readonly Framework framework; private readonly GameGui gameGui; private readonly TitleScreenMenu titleScreenMenu; - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private readonly IFontAtlas privateAtlas; - private readonly Lazy myFontHandle; private readonly Lazy shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); private readonly Dictionary logoEasings = new(); + private readonly Dictionary specialGlyphRequests = new(); private InOutCubic? fadeOutEasing; @@ -52,7 +48,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// An instance of . /// An instance of . /// An instance of . - /// An instance of . /// An instance of . /// An instance of . /// An instance of . @@ -60,7 +55,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable ClientState clientState, DalamudConfiguration configuration, DalamudAssetManager dalamudAssetManager, - FontAtlasFactory fontAtlasFactory, Framework framework, GameGui gameGui, TitleScreenMenu titleScreenMenu) @@ -71,6 +65,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable { this.clientState = clientState; this.configuration = configuration; + this.framework = framework; this.gameGui = gameGui; this.titleScreenMenu = titleScreenMenu; @@ -82,25 +77,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; - this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); - this.privateAtlas = fontAtlasFactory.CreateFontAtlas(this.WindowName, FontAtlasAutoRebuildMode.Async); - this.scopedFinalizer.Add(this.privateAtlas); - - this.myFontHandle = new( - () => this.scopedFinalizer.Add( - this.privateAtlas.NewDelegateFontHandle( - e => e.OnPreBuild( - toolkit => toolkit.AddDalamudDefaultFont( - TargetFontSizePx, - titleScreenMenu.Entries.SelectMany(x => x.Name).ToGlyphRange()))))); - - titleScreenMenu.EntryListChange += this.TitleScreenMenuEntryListChange; - this.scopedFinalizer.Add(() => titleScreenMenu.EntryListChange -= this.TitleScreenMenuEntryListChange); - this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); framework.Update += this.FrameworkOnUpdate; - this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate); } private enum State @@ -115,9 +94,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public bool AllowDrawing { get; set; } = true; - /// - public void Dispose() => this.scopedFinalizer.Dispose(); - /// public override void PreDraw() { @@ -133,6 +109,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable base.PostDraw(); } + /// + public void Dispose() + { + this.framework.Update -= this.FrameworkOnUpdate; + } + /// public override void Draw() { @@ -264,12 +246,33 @@ internal class TitleScreenMenuWindow : Window, IDisposable break; } } + + var srcText = entries.Select(e => e.Name).ToHashSet(); + var keys = this.specialGlyphRequests.Keys.ToHashSet(); + keys.RemoveWhere(x => srcText.Contains(x)); + foreach (var key in keys) + { + this.specialGlyphRequests[key].Dispose(); + this.specialGlyphRequests.Remove(key); + } } private bool DrawEntry( TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) { - using var fontScopeDispose = this.myFontHandle.Value.Push(); + InterfaceManager.SpecialGlyphRequest fontHandle; + if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx) + { + fontHandle.Dispose(); + this.specialGlyphRequests.Remove(entry.Name); + fontHandle = null; + } + + if (fontHandle == null) + this.specialGlyphRequests[entry.Name] = fontHandle = Service.Get().NewFontSizeRef(TargetFontSizePx, entry.Name); + + ImGui.PushFont(fontHandle.Font); + ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size); var scale = ImGui.GetIO().FontGlobalScale; @@ -380,6 +383,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable initialCursor.Y += entry.Texture.Height * scale; ImGui.SetCursorPos(initialCursor); + ImGui.PopFont(); + return isHover; } @@ -396,6 +401,4 @@ internal class TitleScreenMenuWindow : Window, IDisposable if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero) this.IsOpen = false; } - - private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs deleted file mode 100644 index 50e591390..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// How to rebuild . -/// -public enum FontAtlasAutoRebuildMode -{ - /// - /// Do not rebuild. - /// - Disable, - - /// - /// Rebuild on new frame. - /// - OnNewFrame, - - /// - /// Rebuild asynchronously. - /// - Async, -} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs deleted file mode 100644 index 345ab729d..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs +++ /dev/null @@ -1,38 +0,0 @@ -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Build step for . -/// -public enum FontAtlasBuildStep -{ - /// - /// An invalid value. This should never be passed through event callbacks. - /// - Invalid, - - /// - /// Called before calling .
- /// Expect to be passed. - ///
- PreBuild, - - /// - /// Called after calling .
- /// Expect to be passed.
- ///
- /// This callback is not guaranteed to happen after , - /// but it will never happen on its own. - ///
- PostBuild, - - /// - /// Called after promoting staging font atlas to the actual atlas for .
- /// Expect to be passed.
- ///
- /// This callback is not guaranteed to happen after , - /// but it will never happen on its own. - ///
- PostPromotion, -} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs deleted file mode 100644 index 4f5b34061..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Delegate to be called when a font needs to be built. -/// -/// A toolkit that may help you for font building steps. -/// -/// An implementation of may implement all of -/// , , and -/// .
-/// Either use to identify the build step, or use -/// , , -/// and for routing. -///
-public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs deleted file mode 100644 index 586887a3b..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -using Dalamud.Interface.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Convenience function for building fonts through . -/// -public static class FontAtlasBuildToolkitUtilities -{ - /// - /// Compiles given s into an array of containing ImGui glyph ranges. - /// - /// The chars. - /// Add fallback codepoints to the range. - /// Add ellipsis codepoints to the range. - /// The compiled range. - public static ushort[] ToGlyphRange( - this IEnumerable enumerable, - bool addFallbackCodepoints = true, - bool addEllipsisCodepoints = true) - { - using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); - foreach (var c in enumerable) - builder.AddChar(c); - return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); - } - - /// - /// Compiles given s into an array of containing ImGui glyph ranges. - /// - /// The chars. - /// Add fallback codepoints to the range. - /// Add ellipsis codepoints to the range. - /// The compiled range. - public static ushort[] ToGlyphRange( - this ReadOnlySpan span, - bool addFallbackCodepoints = true, - bool addEllipsisCodepoints = true) - { - using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); - foreach (var c in span) - builder.AddChar(c); - return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); - } - - /// - /// Compiles given string into an array of containing ImGui glyph ranges. - /// - /// The string. - /// Add fallback codepoints to the range. - /// Add ellipsis codepoints to the range. - /// The compiled range. - public static ushort[] ToGlyphRange( - this string @string, - bool addFallbackCodepoints = true, - bool addEllipsisCodepoints = true) => - @string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints); - - /// - /// Finds the corresponding in - /// . that corresponds to the - /// specified font . - /// - /// The toolkit. - /// The font. - /// The relevant config pointer, or empty config pointer if not found. - public static unsafe ImFontConfigPtr FindConfigPtr(this IFontAtlasBuildToolkit toolkit, ImFontPtr fontPtr) - { - foreach (ref var c in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan) - { - if (c.DstFont == fontPtr.NativePtr) - return new((nint)Unsafe.AsPointer(ref c)); - } - - return default; - } - - /// - /// Invokes - /// if of - /// is . - /// - /// The toolkit. - /// The action. - /// This, for method chaining. - public static IFontAtlasBuildToolkit OnPreBuild( - this IFontAtlasBuildToolkit toolkit, - Action action) - { - if (toolkit.BuildStep is FontAtlasBuildStep.PreBuild) - action.Invoke((IFontAtlasBuildToolkitPreBuild)toolkit); - return toolkit; - } - - /// - /// Invokes - /// if of - /// is . - /// - /// The toolkit. - /// The action. - /// toolkit, for method chaining. - public static IFontAtlasBuildToolkit OnPostBuild( - this IFontAtlasBuildToolkit toolkit, - Action action) - { - if (toolkit.BuildStep is FontAtlasBuildStep.PostBuild) - action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit); - return toolkit; - } - - /// - /// Invokes - /// if of - /// is . - /// - /// The toolkit. - /// The action. - /// toolkit, for method chaining. - public static IFontAtlasBuildToolkit OnPostPromotion( - this IFontAtlasBuildToolkit toolkit, - Action action) - { - if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion) - action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit); - return toolkit; - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs deleted file mode 100644 index ec3e66e9a..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.Threading.Tasks; - -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Wrapper for . -/// -public interface IFontAtlas : IDisposable -{ - /// - /// Event to be called on build step changes.
- /// is meaningless for this event. - ///
- event FontAtlasBuildStepDelegate? BuildStepChange; - - /// - /// Event fired when a font rebuild operation is recommended.
- /// This event will be invoked from the main thread.
- ///
- /// Reasons for the event include changes in and - /// initialization of new associated font handles. - ///
- /// - /// You should call or - /// if is not set to true.
- /// Avoid calling here; it will block the main thread. - ///
- event Action? RebuildRecommend; - - /// - /// Gets the name of the atlas. For logging and debugging purposes. - /// - string Name { get; } - - /// - /// Gets a value how the atlas should be rebuilt when the relevant Dalamud Configuration changes. - /// - FontAtlasAutoRebuildMode AutoRebuildMode { get; } - - /// - /// Gets the font atlas. Might be empty. - /// - ImFontAtlasPtr ImAtlas { get; } - - /// - /// Gets the task that represents the current font rebuild state. - /// - Task BuildTask { get; } - - /// - /// Gets a value indicating whether there exists any built atlas, regardless of . - /// - bool HasBuiltAtlas { get; } - - /// - /// Gets a value indicating whether this font atlas is under the effect of global scale. - /// - bool IsGlobalScaled { get; } - - /// - /// Suppresses automatically rebuilding fonts for the scope. - /// - /// An instance of that will release the suppression. - /// - /// Use when you will be creating multiple new handles, and want rebuild to trigger only when you're done doing so. - /// This function will effectively do nothing, if is set to - /// . - /// - /// - /// - /// using (atlas.SuppressBuild()) { - /// this.font1 = atlas.NewGameFontHandle(...); - /// this.font2 = atlas.NewDelegateFontHandle(...); - /// } - /// - /// - public IDisposable SuppressAutoRebuild(); - - /// - /// Creates a new from game's built-in fonts. - /// - /// Font to use. - /// Handle to a font that may or may not be ready yet. - public IFontHandle NewGameFontHandle(GameFontStyle style); - - /// - /// Creates a new IFontHandle using your own callbacks. - /// - /// Callback for . - /// Handle to a font that may or may not be ready yet. - /// - /// On initialization: - /// - /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => { - /// var config = new SafeFontConfig { SizePx = 16 }; - /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); - /// tk.AddGameSymbol(config); - /// tk.AddExtraGlyphsForDalamudLanguage(config); - /// // optionally do the following if you have to add more than one font here, - /// // to specify which font added during this delegate is the final font to use. - /// tk.Font = config.MergeFont; - /// })); - /// // or - /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); - /// - ///
- /// On use: - /// - /// using (this.fontHandle.Push()) - /// ImGui.TextUnformatted("Example"); - /// - ///
- public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate); - - /// - /// Queues rebuilding fonts, on the main thread.
- /// Note that would not necessarily get changed from calling this function. - ///
- /// If is . - void BuildFontsOnNextFrame(); - - /// - /// Rebuilds fonts immediately, on the current thread.
- /// Even the callback for will be called on the same thread. - ///
- /// If is . - void BuildFontsImmediately(); - - /// - /// Rebuilds fonts asynchronously, on any thread. - /// - /// Call on the main thread. - /// The task. - /// If is . - Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs deleted file mode 100644 index 4b016bbb2..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Runtime.InteropServices; - -using Dalamud.Interface.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Common stuff for and . -/// -public interface IFontAtlasBuildToolkit -{ - /// - /// Gets or sets the font relevant to the call. - /// - ImFontPtr Font { get; set; } - - /// - /// Gets the current scale this font atlas is being built with. - /// - float Scale { get; } - - /// - /// Gets a value indicating whether the current build operation is asynchronous. - /// - bool IsAsyncBuildOperation { get; } - - /// - /// Gets the current build step. - /// - FontAtlasBuildStep BuildStep { get; } - - /// - /// Gets the font atlas being built. - /// - ImFontAtlasPtr NewImAtlas { get; } - - /// - /// Gets the wrapper for of .
- /// This does not need to be disposed. Calling does nothing.- - ///
- /// Modification of this vector may result in undefined behaviors. - ///
- ImVectorWrapper Fonts { get; } - - /// - /// Queues an item to be disposed after the native atlas gets disposed, successful or not. - /// - /// Disposable type. - /// The disposable. - /// The same . - T DisposeWithAtlas(T disposable) where T : IDisposable; - - /// - /// Queues an item to be disposed after the native atlas gets disposed, successful or not. - /// - /// The gc handle. - /// The same . - GCHandle DisposeWithAtlas(GCHandle gcHandle); - - /// - /// Queues an item to be disposed after the native atlas gets disposed, successful or not. - /// - /// The action to run on dispose. - void DisposeWithAtlas(Action action); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs deleted file mode 100644 index 3c14197e0..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Dalamud.Interface.Internal; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Toolkit for use when the build state is . -/// -public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit -{ - /// - /// Gets whether global scaling is ignored for the given font. - /// - /// The font. - /// True if ignored. - bool IsGlobalScaleIgnored(ImFontPtr fontPtr); - - /// - /// Stores a texture to be managed with the atlas. - /// - /// The texture wrap. - /// Dispose the wrap on error. - /// The texture index. - int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs deleted file mode 100644 index 8c3c91624..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs +++ /dev/null @@ -1,33 +0,0 @@ -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Toolkit for use when the build state is . -/// -public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit -{ - /// - /// Copies glyphs across fonts, in a safer way.
- /// If the font does not belong to the current atlas, this function is a no-op. - ///
- /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - /// Low codepoint range to copy. - /// High codepoing range to copy. - void CopyGlyphsAcrossFonts( - ImFontPtr source, - ImFontPtr target, - bool missingOnly, - bool rebuildLookupTable = true, - char rangeLow = ' ', - char rangeHigh = '\uFFFE'); - - /// - /// Calls , with some fixups. - /// - /// The font. - void BuildLookupTable(ImFontPtr font); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs deleted file mode 100644 index cb8a27a54..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System.IO; -using System.Runtime.InteropServices; - -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Toolkit for use when the build state is .
-///
-/// After returns, -/// either must be set, -/// or at least one font must have been added to the atlas using one of AddFont... functions. -///
-public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit -{ - /// - /// Queues an item to be disposed after the whole build process gets complete, successful or not. - /// - /// Disposable type. - /// The disposable. - /// The same . - T DisposeAfterBuild(T disposable) where T : IDisposable; - - /// - /// Queues an item to be disposed after the whole build process gets complete, successful or not. - /// - /// The gc handle. - /// The same . - GCHandle DisposeAfterBuild(GCHandle gcHandle); - - /// - /// Queues an item to be disposed after the whole build process gets complete, successful or not. - /// - /// The action to run on dispose. - void DisposeAfterBuild(Action action); - - /// - /// Excludes given font from global scaling. - /// - /// The font. - /// Same with . - ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr); - - /// - /// Gets whether global scaling is ignored for the given font. - /// - /// The font. - /// True if ignored. - bool IsGlobalScaleIgnored(ImFontPtr fontPtr); - - /// - /// Adds a font from memory region allocated using .
- /// It WILL crash if you try to use a memory pointer allocated in some other way.
- /// - /// Do NOT call on the once this function has - /// been called, unless is set and the function has thrown an error. - /// - ///
- /// Memory address for the data allocated using . - /// The size of the font file.. - /// The font config. - /// Free if an exception happens. - /// A debug tag. - /// The newly added font. - unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( - nint dataPointer, - int dataSize, - in SafeFontConfig fontConfig, - bool freeOnException, - string debugTag) - => this.AddFontFromImGuiHeapAllocatedMemory( - (void*)dataPointer, - dataSize, - fontConfig, - freeOnException, - debugTag); - - /// - /// Adds a font from memory region allocated using .
- /// It WILL crash if you try to use a memory pointer allocated in some other way.
- /// Do NOT call on the once this - /// function has been called. - ///
- /// Memory address for the data allocated using . - /// The size of the font file.. - /// The font config. - /// Free if an exception happens. - /// A debug tag. - /// The newly added font. - unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( - void* dataPointer, - int dataSize, - in SafeFontConfig fontConfig, - bool freeOnException, - string debugTag); - - /// - /// Adds a font from a file. - /// - /// The file path to create a new font from. - /// The font config. - /// The newly added font. - ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig); - - /// - /// Adds a font from a stream. - /// - /// The stream to create a new font from. - /// The font config. - /// Dispose when this function returns or throws. - /// A debug tag. - /// The newly added font. - ImFontPtr AddFontFromStream(Stream stream, in SafeFontConfig fontConfig, bool leaveOpen, string debugTag); - - /// - /// Adds a font from memory. - /// - /// The span to create from. - /// The font config. - /// A debug tag. - /// The newly added font. - ImFontPtr AddFontFromMemory(ReadOnlySpan span, in SafeFontConfig fontConfig, string debugTag); - - /// - /// Adds the default font known to the current font atlas.
- ///
- /// Includes and .
- /// As this involves adding multiple fonts, calling this function will set - /// as the return value of this function, if it was empty before. - ///
- /// Font size in pixels. - /// The glyph ranges. Use .ToGlyphRange to build. - /// A font returned from . - ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null); - - /// - /// Adds a font that is shipped with Dalamud.
- ///
- /// Note: if game symbols font file is requested but is unavailable, - /// then it will take the glyphs from game's built-in fonts, and everything in - /// will be ignored but , , - /// and . - ///
- /// The font type. - /// The font config. - /// The added font. - ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig); - - /// - /// Same with (, ...), - /// but using only FontAwesome icon ranges.
- /// will be ignored. - ///
- /// The font config. - /// The added font. - ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig); - - /// - /// Adds the game's symbols into the provided font.
- /// will be ignored.
- /// If the game symbol font file is unavailable, only will be honored. - ///
- /// The font config. - /// The added font. - ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig); - - /// - /// Adds the game glyphs to the font. - /// - /// The font style. - /// The glyph ranges. - /// The font to merge to. If empty, then a new font will be created. - /// The added font. - ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont); - - /// - /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
- /// will be ignored. - ///
- /// The font config. - void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs deleted file mode 100644 index 854594663..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ /dev/null @@ -1,42 +0,0 @@ -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Represents a reference counting handle for fonts. -/// -public interface IFontHandle : IDisposable -{ - /// - /// Represents a reference counting handle for fonts. Dalamud internal use only. - /// - internal interface IInternal : IFontHandle - { - /// - /// Gets the font.
- /// Use of this properly is safe only from the UI thread.
- /// Use if the intended purpose of this property is .
- /// Futures changes may make simple not enough. - ///
- ImFontPtr ImFont { get; } - } - - /// - /// Gets the load exception, if it failed to load. Otherwise, it is null. - /// - Exception? LoadException { get; } - - /// - /// Gets a value indicating whether this font is ready for use.
- /// Use directly if you want to keep the current ImGui font if the font is not ready. - ///
- bool Available { get; } - - /// - /// Pushes the current font into ImGui font stack using , if available.
- /// Use to access the current font.
- /// You may not access the font once you dispose this object. - ///
- /// A disposable object that will call (1) on dispose. - IDisposable Push(); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs deleted file mode 100644 index f0ed09155..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Logging.Internal; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// A font handle representing a user-callback generated font. -/// -internal class DelegateFontHandle : IFontHandle.IInternal -{ - private IFontHandleManager? manager; - - /// - /// Initializes a new instance of the class. - /// - /// An instance of . - /// Callback for . - public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange) - { - this.manager = manager; - this.CallOnBuildStepChange = callOnBuildStepChange; - } - - /// - /// Gets the function to be called on build step changes. - /// - public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; } - - /// - public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); - - /// - public bool Available => this.ImFont.IsNotNullAndLoaded(); - - /// - public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; - - private IFontHandleManager ManagerNotDisposed => - this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); - - /// - public void Dispose() - { - this.manager?.FreeFontHandle(this); - this.manager = null; - } - - /// - public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); - - /// - /// Manager for s. - /// - internal sealed class HandleManager : IFontHandleManager - { - private readonly HashSet handles = new(); - private readonly object syncRoot = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The name of the owner atlas. - public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager"; - - /// - public event Action? RebuildRecommend; - - /// - public string Name { get; } - - /// - public IFontHandleSubstance? Substance { get; set; } - - /// - public void Dispose() - { - lock (this.syncRoot) - { - this.handles.Clear(); - this.Substance?.Dispose(); - this.Substance = null; - } - } - - /// - public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) - { - var key = new DelegateFontHandle(this, buildStepDelegate); - lock (this.syncRoot) - this.handles.Add(key); - this.RebuildRecommend?.Invoke(); - return key; - } - - /// - public void FreeFontHandle(IFontHandle handle) - { - if (handle is not DelegateFontHandle cgfh) - return; - - lock (this.syncRoot) - this.handles.Remove(cgfh); - } - - /// - public IFontHandleSubstance NewSubstance() - { - lock (this.syncRoot) - return new HandleSubstance(this, this.handles.ToArray()); - } - } - - /// - /// Substance from . - /// - internal sealed class HandleSubstance : IFontHandleSubstance - { - private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); - - // Not owned by this class. Do not dispose. - private readonly DelegateFontHandle[] relevantHandles; - - // Owned by this class, but ImFontPtr values still do not belong to this. - private readonly Dictionary fonts = new(); - private readonly Dictionary buildExceptions = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The manager. - /// The relevant handles. - public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) - { - this.Manager = manager; - this.relevantHandles = relevantHandles; - } - - /// - public IFontHandleManager Manager { get; } - - /// - public void Dispose() - { - this.fonts.Clear(); - this.buildExceptions.Clear(); - } - - /// - public ImFontPtr GetFontPtr(IFontHandle handle) => - handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default; - - /// - public Exception? GetBuildException(IFontHandle handle) => - handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default; - - /// - public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) - { - var fontsVector = toolkitPreBuild.Fonts; - foreach (var k in this.relevantHandles) - { - var fontCountPrevious = fontsVector.Length; - - try - { - toolkitPreBuild.Font = default; - k.CallOnBuildStepChange(toolkitPreBuild); - if (toolkitPreBuild.Font.IsNull()) - { - if (fontCountPrevious == fontsVector.Length) - { - throw new InvalidOperationException( - $"{nameof(FontAtlasBuildStepDelegate)} must either set the " + - $"{nameof(IFontAtlasBuildToolkitPreBuild.Font)} property, or add at least one font."); - } - - toolkitPreBuild.Font = fontsVector[^1]; - } - else - { - var found = false; - unsafe - { - for (var i = fontCountPrevious; !found && i < fontsVector.Length; i++) - { - if (fontsVector[i].NativePtr == toolkitPreBuild.Font.NativePtr) - found = true; - } - } - - if (!found) - { - throw new InvalidOperationException( - "The font does not exist in the atlas' font array. If you need an empty font, try" + - "adding Noto Sans from Dalamud Assets, but using new ushort[]{ ' ', ' ', 0 } as the" + - "glyph range."); - } - } - - if (fontsVector.Length - fontCountPrevious != 1) - { - Log.Warning( - "[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " + - "Using the most recently added font. " + - "Did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", - this.Manager.Name, - fontsVector.Length - fontCountPrevious, - nameof(FontAtlasBuildStepDelegate), - nameof(SafeFontConfig), - nameof(SafeFontConfig.MergeFont), - nameof(ImFontConfigPtr), - nameof(ImFontConfigPtr.MergeMode)); - } - - for (var i = fontCountPrevious; i < fontsVector.Length; i++) - { - if (fontsVector[i].ValidateUnsafe() is { } ex) - { - throw new InvalidOperationException( - "One of the newly added fonts seem to be pointing to an invalid memory address.", - ex); - } - } - - // Check for duplicate entries; duplicates will result in free-after-free - for (var i = 0; i < fontCountPrevious; i++) - { - for (var j = fontCountPrevious; j < fontsVector.Length; j++) - { - unsafe - { - if (fontsVector[i].NativePtr == fontsVector[j].NativePtr) - throw new InvalidOperationException("An already added font has been added again."); - } - } - } - - this.fonts[k] = toolkitPreBuild.Font; - } - catch (Exception e) - { - this.fonts[k] = default; - this.buildExceptions[k] = e; - - Log.Error( - e, - "[{name}:Substance] An error has occurred while during {delegate} PreBuild call.", - this.Manager.Name, - nameof(FontAtlasBuildStepDelegate)); - - // Sanitization, in a futile attempt to prevent crashes on invalid parameters - unsafe - { - var distinct = - fontsVector - .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates - .Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them - .ToArray(); - - // We're adding the contents back; do not destroy the contents - fontsVector.Clear(true); - fontsVector.AddRange(distinct.AsSpan()); - } - } - } - } - - /// - public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) - { - // irrelevant - } - - /// - public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) - { - foreach (var k in this.relevantHandles) - { - if (!this.fonts[k].IsNotNullAndLoaded()) - continue; - - try - { - toolkitPostBuild.Font = this.fonts[k]; - k.CallOnBuildStepChange.Invoke(toolkitPostBuild); - } - catch (Exception e) - { - this.fonts[k] = default; - this.buildExceptions[k] = e; - - Log.Error( - e, - "[{name}] An error has occurred while during {delegate} PostBuild call.", - this.Manager.Name, - nameof(FontAtlasBuildStepDelegate)); - } - } - } - - /// - public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) - { - foreach (var k in this.relevantHandles) - { - if (!this.fonts[k].IsNotNullAndLoaded()) - continue; - - try - { - toolkitPostPromotion.Font = this.fonts[k]; - k.CallOnBuildStepChange.Invoke(toolkitPostPromotion); - } - catch (Exception e) - { - this.fonts[k] = default; - this.buildExceptions[k] = e; - - Log.Error( - e, - "[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.", - this.Manager.Name, - nameof(FontAtlasBuildStepDelegate)); - } - } - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs deleted file mode 100644 index e73ea7548..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ /dev/null @@ -1,682 +0,0 @@ -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text.Unicode; - -using Dalamud.Configuration.Internal; -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Storage.Assets; -using Dalamud.Utility; - -using ImGuiNET; - -using SharpDX.DXGI; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Standalone font atlas. -/// -internal sealed partial class FontAtlasFactory -{ - private static readonly Dictionary> PairAdjustmentsCache = - new(); - - /// - /// Implementations for and - /// . - /// - private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable - { - private static readonly ushort FontAwesomeIconMin = - (ushort)Enum.GetValues().Where(x => x > 0).Min(); - - private static readonly ushort FontAwesomeIconMax = - (ushort)Enum.GetValues().Where(x => x > 0).Max(); - - private readonly DisposeSafety.ScopedFinalizer disposeAfterBuild = new(); - private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance; - private readonly FontAtlasFactory factory; - private readonly FontAtlasBuiltData data; - - /// - /// Initializes a new instance of the class. - /// - /// An instance of . - /// New atlas. - /// An instance of . - /// Specify whether the current build operation is an asynchronous one. - public BuildToolkit( - FontAtlasFactory factory, - FontAtlasBuiltData data, - GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance, - bool isAsync) - { - this.data = data; - this.gameFontHandleSubstance = gameFontHandleSubstance; - this.IsAsyncBuildOperation = isAsync; - this.factory = factory; - } - - /// - public ImFontPtr Font { get; set; } - - /// - public float Scale => this.data.Scale; - - /// - public bool IsAsyncBuildOperation { get; } - - /// - public FontAtlasBuildStep BuildStep { get; set; } - - /// - public ImFontAtlasPtr NewImAtlas => this.data.Atlas; - - /// - public ImVectorWrapper Fonts => this.data.Fonts; - - /// - /// Gets the list of fonts to ignore global scale. - /// - public List GlobalScaleExclusions { get; } = new(); - - /// - public void Dispose() => this.disposeAfterBuild.Dispose(); - - /// - public T2 DisposeAfterBuild(T2 disposable) where T2 : IDisposable => - this.disposeAfterBuild.Add(disposable); - - /// - public GCHandle DisposeAfterBuild(GCHandle gcHandle) => this.disposeAfterBuild.Add(gcHandle); - - /// - public void DisposeAfterBuild(Action action) => this.disposeAfterBuild.Add(action); - - /// - public T DisposeWithAtlas(T disposable) where T : IDisposable => this.data.Garbage.Add(disposable); - - /// - public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.data.Garbage.Add(gcHandle); - - /// - public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action); - - /// - public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) - { - this.GlobalScaleExclusions.Add(fontPtr); - return fontPtr; - } - - /// - public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => - this.GlobalScaleExclusions.Contains(fontPtr); - - /// - public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => - this.data.AddNewTexture(textureWrap, disposeOnError); - - /// - public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( - void* dataPointer, - int dataSize, - in SafeFontConfig fontConfig, - bool freeOnException, - string debugTag) - { - Log.Verbose( - "[{name}] 0x{atlas:X}: {funcname}(0x{dataPointer:X}, 0x{dataSize:X}, ...) from {tag}", - this.data.Owner?.Name ?? "(error)", - (nint)this.NewImAtlas.NativePtr, - nameof(this.AddFontFromImGuiHeapAllocatedMemory), - (nint)dataPointer, - dataSize, - debugTag); - - try - { - fontConfig.ThrowOnInvalidValues(); - - var raw = fontConfig.Raw with - { - FontData = dataPointer, - FontDataSize = dataSize, - }; - - if (fontConfig.GlyphRanges is not { Length: > 0 } ranges) - ranges = new ushort[] { 1, 0xFFFE, 0 }; - - raw.GlyphRanges = (ushort*)this.DisposeAfterBuild( - GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject(); - - TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw); - - var font = this.NewImAtlas.AddFont(&raw); - - var dataHash = default(HashCode); - dataHash.AddBytes(new(dataPointer, dataSize)); - var hashIdent = (uint)dataHash.ToHashCode() | ((ulong)dataSize << 32); - - List<(char Left, char Right, float Distance)> pairAdjustments; - lock (PairAdjustmentsCache) - { - if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments)) - { - PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new()); - try - { - pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray()); - } - catch - { - // don't care - } - } - } - - foreach (var pair in pairAdjustments) - { - if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Left, raw.GlyphRanges)) - continue; - if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Right, raw.GlyphRanges)) - continue; - - font.AddKerningPair(pair.Left, pair.Right, pair.Distance * raw.SizePixels); - } - - return font; - } - catch - { - if (freeOnException) - ImGuiNative.igMemFree(dataPointer); - throw; - } - } - - /// - public ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig) - { - return this.AddFontFromStream( - File.OpenRead(path), - fontConfig, - false, - $"{nameof(this.AddFontFromFile)}({path})"); - } - - /// - public unsafe ImFontPtr AddFontFromStream( - Stream stream, - in SafeFontConfig fontConfig, - bool leaveOpen, - string debugTag) - { - using var streamCloser = leaveOpen ? null : stream; - if (!stream.CanSeek) - { - // There is no need to dispose a MemoryStream. - var ms = new MemoryStream(); - stream.CopyTo(ms); - stream = ms; - } - - var length = checked((int)(uint)stream.Length); - var memory = ImGuiHelpers.AllocateMemory(length); - try - { - stream.ReadExactly(new(memory, length)); - return this.AddFontFromImGuiHeapAllocatedMemory( - memory, - length, - fontConfig, - false, - $"{nameof(this.AddFontFromStream)}({debugTag})"); - } - catch - { - ImGuiNative.igMemFree(memory); - throw; - } - } - - /// - public unsafe ImFontPtr AddFontFromMemory( - ReadOnlySpan span, - in SafeFontConfig fontConfig, - string debugTag) - { - var length = span.Length; - var memory = ImGuiHelpers.AllocateMemory(length); - try - { - span.CopyTo(new(memory, length)); - return this.AddFontFromImGuiHeapAllocatedMemory( - memory, - length, - fontConfig, - false, - $"{nameof(this.AddFontFromMemory)}({debugTag})"); - } - catch - { - ImGuiNative.igMemFree(memory); - throw; - } - } - - /// - public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges) - { - ImFontPtr font; - glyphRanges ??= this.factory.DefaultGlyphRanges; - if (this.factory.UseAxis) - { - font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); - } - else - { - font = this.AddDalamudAssetFont( - DalamudAsset.NotoSansJpMedium, - new() { SizePx = sizePx, GlyphRanges = glyphRanges }); - this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font }); - } - - this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font }); - if (this.Font.IsNull()) - this.Font = font; - return font; - } - - /// - public ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig) - { - if (asset.GetPurpose() != DalamudAssetPurpose.Font) - throw new ArgumentOutOfRangeException(nameof(asset), asset, "Must have the purpose of Font."); - - switch (asset) - { - case DalamudAsset.LodestoneGameSymbol when this.factory.HasGameSymbolsFontFile: - return this.factory.AddFont( - this, - asset, - fontConfig with - { - FontNo = 0, - SizePx = (fontConfig.SizePx * 3) / 2, - }); - - case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile: - { - return this.AddGameGlyphs( - new(GameFontFamily.Axis, fontConfig.SizePx), - fontConfig.GlyphRanges, - fontConfig.MergeFont); - } - - default: - return this.factory.AddFont( - this, - asset, - fontConfig with - { - FontNo = 0, - }); - } - } - - /// - public ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( - DalamudAsset.FontAwesomeFreeSolid, - fontConfig with - { - GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 }, - }); - - /// - public ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig) => - this.AddDalamudAssetFont( - DalamudAsset.LodestoneGameSymbol, - fontConfig with - { - GlyphRanges = new ushort[] - { - GamePrebakedFontHandle.SeIconCharMin, - GamePrebakedFontHandle.SeIconCharMax, - 0, - }, - }); - - /// - public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) => - this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges); - - /// - public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) - { - var dalamudConfiguration = Service.Get(); - if (dalamudConfiguration.EffectiveLanguage == "ko" - || Service.GetNullable()?.EncounteredHangul is true) - { - this.AddDalamudAssetFont( - DalamudAsset.NotoSansKrRegular, - fontConfig with - { - GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( - UnicodeRanges.HangulJamo, - UnicodeRanges.HangulCompatibilityJamo, - UnicodeRanges.HangulSyllables, - UnicodeRanges.HangulJamoExtendedA, - UnicodeRanges.HangulJamoExtendedB), - }); - } - - var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows); - var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc"); - if (!File.Exists(fontPathChs)) - fontPathChs = null; - - var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc"); - if (!File.Exists(fontPathCht)) - fontPathCht = null; - - if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") - { - this.AddFontFromFile(fontPathCht, fontConfig with - { - GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( - UnicodeRanges.CjkUnifiedIdeographs, - UnicodeRanges.CjkUnifiedIdeographsExtensionA), - }); - } - else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" - || Service.GetNullable()?.EncounteredHan is true)) - { - this.AddFontFromFile(fontPathChs, fontConfig with - { - GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( - UnicodeRanges.CjkUnifiedIdeographs, - UnicodeRanges.CjkUnifiedIdeographsExtensionA), - }); - } - } - - public void PreBuildSubstances() - { - foreach (var substance in this.data.Substances) - substance.OnPreBuild(this); - foreach (var substance in this.data.Substances) - substance.OnPreBuildCleanup(this); - } - - public unsafe void PreBuild() - { - var configData = this.data.ConfigData; - foreach (ref var config in configData.DataSpan) - { - if (this.GlobalScaleExclusions.Contains(new(config.DstFont))) - continue; - - config.SizePixels *= this.Scale; - - config.GlyphMaxAdvanceX *= this.Scale; - if (float.IsInfinity(config.GlyphMaxAdvanceX)) - config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue; - - config.GlyphMinAdvanceX *= this.Scale; - if (float.IsInfinity(config.GlyphMinAdvanceX)) - config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue; - - config.GlyphOffset *= this.Scale; - } - } - - public void DoBuild() - { - // ImGui will call AddFontDefault() on Build() call. - // AddFontDefault() will reliably crash, when invoked multithreaded. - // We add a dummy font to prevent that. - if (this.data.ConfigData.Length == 0) - { - this.AddDalamudAssetFont( - DalamudAsset.NotoSansJpMedium, - new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 }); - } - - if (!this.NewImAtlas.Build()) - throw new InvalidOperationException("ImFontAtlas.Build failed"); - - this.BuildStep = FontAtlasBuildStep.PostBuild; - } - - public unsafe void PostBuild() - { - var scale = this.Scale; - foreach (ref var font in this.Fonts.DataSpan) - { - if (!this.GlobalScaleExclusions.Contains(font)) - font.AdjustGlyphMetrics(1 / scale, 1 / scale); - - foreach (var c in FallbackCodepoints) - { - var g = font.FindGlyphNoFallback(c); - if (g.NativePtr == null) - continue; - - font.UpdateFallbackChar(c); - break; - } - - foreach (var c in EllipsisCodepoints) - { - var g = font.FindGlyphNoFallback(c); - if (g.NativePtr == null) - continue; - - font.EllipsisChar = c; - break; - } - } - } - - public void PostBuildSubstances() - { - foreach (var substance in this.data.Substances) - substance.OnPostBuild(this); - } - - public unsafe void UploadTextures() - { - var buf = Array.Empty(); - try - { - var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); - var bpp = use4 ? 2 : 4; - var width = this.NewImAtlas.TexWidth; - var height = this.NewImAtlas.TexHeight; - foreach (ref var texture in this.data.ImTextures.DataSpan) - { - if (texture.TexID != 0) - { - // Nothing to do - } - else if (texture.TexPixelsRGBA32 is not null) - { - var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( - new(texture.TexPixelsRGBA32, width * height * 4), - width * 4, - width, - height, - use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm); - this.data.AddExistingTexture(wrap); - texture.TexID = wrap.ImGuiHandle; - } - else if (texture.TexPixelsAlpha8 is not null) - { - var numPixels = width * height; - if (buf.Length < numPixels * bpp) - { - ArrayPool.Shared.Return(buf); - buf = ArrayPool.Shared.Rent(numPixels * bpp); - } - - fixed (void* pBuf = buf) - { - var sourcePtr = texture.TexPixelsAlpha8; - if (use4) - { - var target = (ushort*)pBuf; - while (numPixels-- > 0) - { - *target = (ushort)((*sourcePtr << 8) | 0x0FFF); - target++; - sourcePtr++; - } - } - else - { - var target = (uint*)pBuf; - while (numPixels-- > 0) - { - *target = (uint)((*sourcePtr << 24) | 0x00FFFFFF); - target++; - sourcePtr++; - } - } - } - - var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( - buf, - width * bpp, - width, - height, - use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm); - this.data.AddExistingTexture(wrap); - texture.TexID = wrap.ImGuiHandle; - continue; - } - else - { - Log.Warning( - "[{name}]: TexID, TexPixelsRGBA32, and TexPixelsAlpha8 are all null", - this.data.Owner?.Name ?? "(error)"); - } - - if (texture.TexPixelsRGBA32 is not null) - ImGuiNative.igMemFree(texture.TexPixelsRGBA32); - if (texture.TexPixelsAlpha8 is not null) - ImGuiNative.igMemFree(texture.TexPixelsAlpha8); - texture.TexPixelsRGBA32 = null; - texture.TexPixelsAlpha8 = null; - } - } - finally - { - ArrayPool.Shared.Return(buf); - } - } - } - - /// - /// Implementations for . - /// - private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion - { - private readonly FontAtlasBuiltData builtData; - - /// - /// Initializes a new instance of the class. - /// - /// The built data. - public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; - - /// - public ImFontPtr Font { get; set; } - - /// - public float Scale => this.builtData.Scale; - - /// - public bool IsAsyncBuildOperation => true; - - /// - public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; - - /// - public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; - - /// - public unsafe ImVectorWrapper Fonts => new( - &this.NewImAtlas.NativePtr->Fonts, - x => ImGuiNative.ImFont_destroy(x->NativePtr)); - - /// - public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); - - /// - public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); - - /// - public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); - - /// - public unsafe void CopyGlyphsAcrossFonts( - ImFontPtr source, - ImFontPtr target, - bool missingOnly, - bool rebuildLookupTable = true, - char rangeLow = ' ', - char rangeHigh = '\uFFFE') - { - var sourceFound = false; - var targetFound = false; - foreach (var f in this.Fonts) - { - sourceFound |= f.NativePtr == source.NativePtr; - targetFound |= f.NativePtr == target.NativePtr; - } - - if (sourceFound && targetFound) - { - ImGuiHelpers.CopyGlyphsAcrossFonts( - source, - target, - missingOnly, - false, - rangeLow, - rangeHigh); - if (rebuildLookupTable) - this.BuildLookupTable(target); - } - } - - /// - public unsafe void BuildLookupTable(ImFontPtr font) - { - // Need to clear previous Fallback pointers before BuildLookupTable, or it may crash - font.NativePtr->FallbackGlyph = null; - font.NativePtr->FallbackHotData = null; - font.BuildLookupTable(); - - // Need to fix our custom ImGui, so that imgui_widgets.cpp:3656 stops thinking - // Codepoint < FallbackHotData.size always means that it's not fallback char. - // Otherwise, having a fallback character in ImGui.InputText gets strange. - var indexedHotData = font.IndexedHotDataWrapped(); - var indexLookup = font.IndexLookupWrapped(); - ref var fallbackHotData = ref *(ImGuiHelpers.ImFontGlyphHotDataReal*)font.NativePtr->FallbackHotData; - for (var codepoint = 0; codepoint < indexedHotData.Length; codepoint++) - { - if (indexLookup[codepoint] == ushort.MaxValue) - { - indexedHotData[codepoint].AdvanceX = fallbackHotData.AdvanceX; - indexedHotData[codepoint].OccupiedWidth = fallbackHotData.OccupiedWidth; - } - } - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs deleted file mode 100644 index 5656fc673..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ /dev/null @@ -1,726 +0,0 @@ -// #define VeryVerboseLog - -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Disposables; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; -using Dalamud.Utility; - -using ImGuiNET; - -using JetBrains.Annotations; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Standalone font atlas. -/// -internal sealed partial class FontAtlasFactory -{ - /// - /// Fallback codepoints for ImFont. - /// - public const string FallbackCodepoints = "\u3013\uFFFD?-"; - - /// - /// Ellipsis codepoints for ImFont. - /// - public const string EllipsisCodepoints = "\u2026\u0085"; - - /// - /// If set, disables concurrent font build operation. - /// - private static readonly object? NoConcurrentBuildOperationLock = null; // new(); - - private static readonly ModuleLog Log = new(nameof(FontAtlasFactory)); - - private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); - - private struct FontAtlasBuiltData : IDisposable - { - public readonly DalamudFontAtlas? Owner; - public readonly ImFontAtlasPtr Atlas; - public readonly float Scale; - - public bool IsBuildInProgress; - - private readonly List? wraps; - private readonly List? substances; - private readonly DisposeSafety.ScopedFinalizer? garbage; - - public unsafe FontAtlasBuiltData( - DalamudFontAtlas owner, - IEnumerable substances, - float scale) - { - this.Owner = owner; - this.Scale = scale; - this.garbage = new(); - - try - { - var substancesList = this.substances = new(); - foreach (var s in substances) - substancesList.Add(this.garbage.Add(s)); - this.garbage.Add(() => substancesList.Clear()); - - var wrapsCopy = this.wraps = new(); - this.garbage.Add(() => wrapsCopy.Clear()); - - var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas(); - this.Atlas = atlasPtr; - if (this.Atlas.NativePtr is null) - throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}."); - - this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); - this.IsBuildInProgress = true; - } - catch - { - this.garbage.Dispose(); - throw; - } - } - - public readonly DisposeSafety.ScopedFinalizer Garbage => - this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); - - public readonly ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); - - public readonly ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); - - public readonly ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); - - public readonly IReadOnlyList Wraps => - (IReadOnlyList?)this.wraps ?? Array.Empty(); - - public readonly IReadOnlyList Substances => - (IReadOnlyList?)this.substances ?? Array.Empty(); - - public readonly void AddExistingTexture(IDalamudTextureWrap wrap) - { - if (this.wraps is null) - throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); - - this.wraps.Add(this.Garbage.Add(wrap)); - } - - public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) - { - if (this.wraps is null) - throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); - - var handle = wrap.ImGuiHandle; - var index = this.ImTextures.IndexOf(x => x.TexID == handle); - if (index == -1) - { - try - { - this.wraps.EnsureCapacity(this.wraps.Count + 1); - this.ImTextures.EnsureCapacityExponential(this.ImTextures.Length + 1); - - index = this.ImTextures.Length; - this.wraps.Add(this.Garbage.Add(wrap)); - this.ImTextures.Add(new() { TexID = handle }); - } - catch (Exception e) - { - if (disposeOnError) - wrap.Dispose(); - - if (this.wraps.Count != this.ImTextures.Length) - { - Log.Error( - e, - "{name} failed, and {wraps} and {imtextures} have different number of items", - nameof(this.AddNewTexture), - nameof(this.Wraps), - nameof(this.ImTextures)); - - if (this.wraps.Count > 0 && this.wraps[^1] == wrap) - this.wraps.RemoveAt(this.wraps.Count - 1); - if (this.ImTextures.Length > 0 && this.ImTextures[^1].TexID == handle) - this.ImTextures.RemoveAt(this.ImTextures.Length - 1); - - if (this.wraps.Count != this.ImTextures.Length) - Log.Fatal("^ Failed to undo due to an internal inconsistency; embrace for a crash"); - } - - throw; - } - } - - return index; - } - - public unsafe void Dispose() - { - if (this.garbage is null) - return; - - if (this.IsBuildInProgress) - { - Log.Error( - "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + - "Stack:\n{trace}", - this.Owner?.Name ?? "", - (nint)this.Atlas.NativePtr, - new StackTrace()); - while (this.IsBuildInProgress) - Thread.Sleep(100); - } - -#if VeryVerboseLog - Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); -#endif - this.garbage.Dispose(); - } - - public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) - { - var axisSubstance = this.Substances.OfType().Single(); - return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild }; - } - } - - private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback - { - private readonly DisposeSafety.ScopedFinalizer disposables = new(); - private readonly FontAtlasFactory factory; - private readonly DelegateFontHandle.HandleManager delegateFontHandleManager; - private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager; - private readonly IFontHandleManager[] fontHandleManagers; - - private readonly object syncRootPostPromotion = new(); - private readonly object syncRoot = new(); - - private Task buildTask = EmptyTask; - private FontAtlasBuiltData builtData; - - private int buildSuppressionCounter; - private bool buildSuppressionSuppressed; - - private int buildIndex; - private bool buildQueued; - private bool disposed = false; - - /// - /// Initializes a new instance of the class. - /// - /// The factory. - /// Name of atlas, for debugging and logging purposes. - /// Specify how to auto rebuild. - /// Whether the fonts in the atlas are under the effect of global scale. - public DalamudFontAtlas( - FontAtlasFactory factory, - string atlasName, - FontAtlasAutoRebuildMode autoRebuildMode, - bool isGlobalScaled) - { - this.IsGlobalScaled = isGlobalScaled; - try - { - this.factory = factory; - this.AutoRebuildMode = autoRebuildMode; - this.Name = atlasName; - - this.factory.InterfaceManager.AfterBuildFonts += this.OnRebuildRecommend; - this.disposables.Add(() => this.factory.InterfaceManager.AfterBuildFonts -= this.OnRebuildRecommend); - - this.fontHandleManagers = new IFontHandleManager[] - { - this.delegateFontHandleManager = this.disposables.Add( - new DelegateFontHandle.HandleManager(atlasName)), - this.gameFontHandleManager = this.disposables.Add( - new GamePrebakedFontHandle.HandleManager(atlasName, factory)), - }; - foreach (var fhm in this.fontHandleManagers) - fhm.RebuildRecommend += this.OnRebuildRecommend; - } - catch - { - this.disposables.Dispose(); - throw; - } - - this.factory.SceneTask.ContinueWith( - r => - { - lock (this.syncRoot) - { - if (this.disposed) - return; - - r.Result.OnNewRenderFrame += this.ImGuiSceneOnNewRenderFrame; - this.disposables.Add(() => r.Result.OnNewRenderFrame -= this.ImGuiSceneOnNewRenderFrame); - } - - if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) - this.BuildFontsOnNextFrame(); - }); - } - - /// - /// Finalizes an instance of the class. - /// - ~DalamudFontAtlas() - { - lock (this.syncRoot) - { - this.buildTask.ToDisposableIgnoreExceptions().Dispose(); - this.builtData.Dispose(); - } - } - - /// - public event FontAtlasBuildStepDelegate? BuildStepChange; - - /// - public event Action? RebuildRecommend; - - /// - public event Action? BeforeDispose; - - /// - public event Action? AfterDispose; - - /// - public string Name { get; } - - /// - public FontAtlasAutoRebuildMode AutoRebuildMode { get; } - - /// - public ImFontAtlasPtr ImAtlas - { - get - { - lock (this.syncRoot) - return this.builtData.Atlas; - } - } - - /// - public Task BuildTask => this.buildTask; - - /// - public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); - - /// - public bool IsGlobalScaled { get; } - - /// - public void Dispose() - { - if (this.disposed) - return; - - this.BeforeDispose?.InvokeSafely(this); - - try - { - lock (this.syncRoot) - { - this.disposed = true; - this.buildTask.ToDisposableIgnoreExceptions().Dispose(); - this.buildTask = EmptyTask; - this.disposables.Add(this.builtData); - this.builtData = default; - this.disposables.Dispose(); - } - - try - { - this.AfterDispose?.Invoke(this, null); - } - catch - { - // ignore - } - } - catch (Exception e) - { - try - { - this.AfterDispose?.Invoke(this, e); - } - catch - { - // ignore - } - } - - GC.SuppressFinalize(this); - } - - /// - public IDisposable SuppressAutoRebuild() - { - this.buildSuppressionCounter++; - return Disposable.Create( - () => - { - this.buildSuppressionCounter--; - if (this.buildSuppressionSuppressed) - this.OnRebuildRecommend(); - }); - } - - /// - public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); - - /// - public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => - this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); - - /// - public void BuildFontsOnNextFrame() - { - if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) - { - throw new InvalidOperationException( - $"{nameof(this.BuildFontsOnNextFrame)} cannot be used when " + - $"{nameof(this.AutoRebuildMode)} is set to " + - $"{nameof(FontAtlasAutoRebuildMode.Async)}."); - } - - if (!this.buildTask.IsCompleted || this.buildQueued) - return; - -#if VeryVerboseLog - Log.Verbose("[{name}] Queueing from {source}.", this.Name, nameof(this.BuildFontsOnNextFrame)); -#endif - - this.buildQueued = true; - } - - /// - public void BuildFontsImmediately() - { -#if VeryVerboseLog - Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsImmediately)); -#endif - - if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) - { - throw new InvalidOperationException( - $"{nameof(this.BuildFontsImmediately)} cannot be used when " + - $"{nameof(this.AutoRebuildMode)} is set to " + - $"{nameof(FontAtlasAutoRebuildMode.Async)}."); - } - - var tcs = new TaskCompletionSource(); - int rebuildIndex; - try - { - rebuildIndex = ++this.buildIndex; - lock (this.syncRoot) - { - if (!this.buildTask.IsCompleted) - throw new InvalidOperationException("Font rebuild is already in progress."); - - this.buildTask = tcs.Task; - } - -#if VeryVerboseLog - Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsImmediately)); -#endif - - var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; - var r = this.RebuildFontsPrivate(false, scale); - r.Wait(); - if (r.IsCompletedSuccessfully) - tcs.SetResult(r.Result); - else if (r.Exception is not null) - tcs.SetException(r.Exception); - else - tcs.SetCanceled(); - } - catch (Exception e) - { - tcs.SetException(e); - Log.Error(e, "[{name}] Failed to build fonts.", this.Name); - throw; - } - - this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately)); - } - - /// - public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true) - { -#if VeryVerboseLog - Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync)); -#endif - - if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) - { - throw new InvalidOperationException( - $"{nameof(this.BuildFontsAsync)} cannot be used when " + - $"{nameof(this.AutoRebuildMode)} is set to " + - $"{nameof(FontAtlasAutoRebuildMode.OnNewFrame)}."); - } - - lock (this.syncRoot) - { - var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; - var rebuildIndex = ++this.buildIndex; - return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); - - async Task BuildInner(Task unused) - { - Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync)); - lock (this.syncRoot) - { - if (this.buildIndex != rebuildIndex) - return default; - } - - var res = await this.RebuildFontsPrivate(true, scale); - if (res.Atlas.IsNull()) - return res; - - if (callPostPromotionOnMainThread) - { - await this.factory.Framework.RunOnFrameworkThread( - () => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync))); - } - else - { - this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); - } - - return res; - } - } - } - - private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) - { - lock (this.syncRoot) - { - if (this.buildIndex != rebuildIndex) - { - data.ExplicitDisposeIgnoreExceptions(); - return; - } - - this.builtData.ExplicitDisposeIgnoreExceptions(); - this.builtData = data; - this.buildTask = EmptyTask; - foreach (var substance in data.Substances) - substance.Manager.Substance = substance; - } - - lock (this.syncRootPostPromotion) - { - if (this.buildIndex != rebuildIndex) - { - data.ExplicitDisposeIgnoreExceptions(); - return; - } - - var toolkit = new BuildToolkitPostPromotion(data); - - try - { - this.BuildStepChange?.Invoke(toolkit); - } - catch (Exception e) - { - Log.Error( - e, - "[{name}] {delegateName} PostPromotion error", - this.Name, - nameof(FontAtlasBuildStepDelegate)); - } - - foreach (var substance in data.Substances) - { - try - { - substance.OnPostPromotion(toolkit); - } - catch (Exception e) - { - Log.Error( - e, - "[{name}] {substance} PostPromotion error", - this.Name, - substance.GetType().FullName ?? substance.GetType().Name); - } - } - - foreach (var font in toolkit.Fonts) - { - try - { - toolkit.BuildLookupTable(font); - } - catch (Exception e) - { - Log.Error(e, "[{name}] BuildLookupTable error", this.Name); - } - } - -#if VeryVerboseLog - Log.Verbose("[{name}] Built from {source}.", this.Name, source); -#endif - } - } - - private void ImGuiSceneOnNewRenderFrame() - { - if (!this.buildQueued) - return; - - try - { - if (this.AutoRebuildMode != FontAtlasAutoRebuildMode.Async) - this.BuildFontsImmediately(); - } - finally - { - this.buildQueued = false; - } - } - - private Task RebuildFontsPrivate(bool isAsync, float scale) - { - if (NoConcurrentBuildOperationLock is null) - return this.RebuildFontsPrivateReal(isAsync, scale); - lock (NoConcurrentBuildOperationLock) - return this.RebuildFontsPrivateReal(isAsync, scale); - } - - private async Task RebuildFontsPrivateReal(bool isAsync, float scale) - { - lock (this.syncRoot) - { - // this lock ensures that this.buildTask is properly set. - } - - var sw = new Stopwatch(); - sw.Start(); - - var res = default(FontAtlasBuiltData); - nint atlasPtr = 0; - try - { - res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); - unsafe - { - atlasPtr = (nint)res.Atlas.NativePtr; - } - - Log.Verbose( - "[{name}:{functionname}] 0x{ptr:X}: PreBuild (at {sw}ms)", - this.Name, - nameof(this.RebuildFontsPrivateReal), - atlasPtr, - sw.ElapsedMilliseconds); - - using var toolkit = res.CreateToolkit(this.factory, isAsync); - this.BuildStepChange?.Invoke(toolkit); - toolkit.PreBuildSubstances(); - toolkit.PreBuild(); - -#if VeryVerboseLog - Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); -#endif - - toolkit.DoBuild(); - -#if VeryVerboseLog - Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: PostBuild (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); -#endif - - toolkit.PostBuild(); - toolkit.PostBuildSubstances(); - this.BuildStepChange?.Invoke(toolkit); - - if (this.factory.SceneTask is { IsCompleted: false } sceneTask) - { - Log.Verbose( - "[{name}:{functionname}] 0x{ptr:X}: await SceneTask (at {sw}ms)", - this.Name, - nameof(this.RebuildFontsPrivateReal), - atlasPtr, - sw.ElapsedMilliseconds); - await sceneTask.ConfigureAwait(!isAsync); - } - -#if VeryVerboseLog - Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: UploadTextures (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); -#endif - toolkit.UploadTextures(); - - Log.Verbose( - "[{name}:{functionname}] 0x{ptr:X}: Complete (at {sw}ms)", - this.Name, - nameof(this.RebuildFontsPrivateReal), - atlasPtr, - sw.ElapsedMilliseconds); - - res.IsBuildInProgress = false; - return res; - } - catch (Exception e) - { - Log.Error( - e, - "[{name}:{functionname}] 0x{ptr:X}: Failed (at {sw}ms)", - this.Name, - nameof(this.RebuildFontsPrivateReal), - atlasPtr, - sw.ElapsedMilliseconds); - res.IsBuildInProgress = false; - res.Dispose(); - throw; - } - finally - { - this.buildQueued = false; - } - } - - private void OnRebuildRecommend() - { - if (this.disposed) - return; - - if (this.buildSuppressionCounter > 0) - { - this.buildSuppressionSuppressed = true; - return; - } - - this.buildSuppressionSuppressed = false; - this.factory.Framework.RunOnFrameworkThread( - () => - { - this.RebuildRecommend?.InvokeSafely(); - - switch (this.AutoRebuildMode) - { - case FontAtlasAutoRebuildMode.Async: - _ = this.BuildFontsAsync(); - break; - case FontAtlasAutoRebuildMode.OnNewFrame: - this.BuildFontsOnNextFrame(); - break; - case FontAtlasAutoRebuildMode.Disable: - default: - break; - } - }); - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs deleted file mode 100644 index 358ccd845..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ /dev/null @@ -1,368 +0,0 @@ -using System.Buffers; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Configuration.Internal; -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal; -using Dalamud.Storage.Assets; -using Dalamud.Utility; - -using ImGuiNET; - -using ImGuiScene; - -using Lumina.Data.Files; - -using SharpDX; -using SharpDX.Direct3D11; -using SharpDX.DXGI; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Factory for the implementation of . -/// -[ServiceManager.BlockingEarlyLoadedService] -internal sealed partial class FontAtlasFactory - : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable -{ - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private readonly CancellationTokenSource cancellationTokenSource = new(); - private readonly IReadOnlyDictionary> fdtFiles; - private readonly IReadOnlyDictionary[]>> texFiles; - private readonly IReadOnlyDictionary> prebakedTextureWraps; - private readonly Task defaultGlyphRanges; - private readonly DalamudAssetManager dalamudAssetManager; - - [ServiceManager.ServiceConstructor] - private FontAtlasFactory( - DataManager dataManager, - Framework framework, - InterfaceManager interfaceManager, - DalamudAssetManager dalamudAssetManager) - { - this.Framework = framework; - this.InterfaceManager = interfaceManager; - this.dalamudAssetManager = dalamudAssetManager; - this.SceneTask = Service - .GetAsync() - .ContinueWith(r => r.Result.Manager.Scene); - - var gffasInfo = Enum.GetValues() - .Select( - x => - ( - Font: x, - Attr: x.GetAttribute())) - .Where(x => x.Attr is not null) - .ToArray(); - var texPaths = gffasInfo.Select(x => x.Attr.TexPathFormat).Distinct().ToArray(); - - this.fdtFiles = gffasInfo.ToImmutableDictionary( - x => x.Font, - x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); - var channelCountsTask = texPaths.ToImmutableDictionary( - x => x, - x => Task.WhenAll( - gffasInfo.Where(y => y.Attr.TexPathFormat == x) - .Select(y => this.fdtFiles[y.Font])) - .ContinueWith( - files => 1 + files.Result.Max( - file => - { - unsafe - { - using var pin = file.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Length); - return fdt.MaxTextureIndex; - } - }))); - this.prebakedTextureWraps = channelCountsTask.ToImmutableDictionary( - x => x.Key, - x => x.Value.ContinueWith(y => new IDalamudTextureWrap?[y.Result])); - this.texFiles = channelCountsTask.ToImmutableDictionary( - x => x.Key, - x => x.Value.ContinueWith( - y => Enumerable - .Range(1, 1 + ((y.Result - 1) / 4)) - .Select(z => Task.Run(() => dataManager.GetFile(string.Format(x.Key, z))!)) - .ToArray())); - this.defaultGlyphRanges = - this.fdtFiles[GameFontFamilyAndSize.Axis12] - .ContinueWith( - file => - { - unsafe - { - using var pin = file.Result.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Result.Length); - return fdt.ToGlyphRanges(); - } - }); - } - - /// - /// Gets or sets a value indicating whether to override configuration for UseAxis. - /// - public bool? UseAxisOverride { get; set; } = null; - - /// - /// Gets a value indicating whether to use AXIS fonts. - /// - public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; - - /// - /// Gets the service instance of . - /// - public Framework Framework { get; } - - /// - /// Gets the service instance of .
- /// may not yet be available. - ///
- public InterfaceManager InterfaceManager { get; } - - /// - /// Gets the async task for inside . - /// - public Task SceneTask { get; } - - /// - /// Gets the default glyph ranges (glyph ranges of ). - /// - public ushort[] DefaultGlyphRanges => ExtractResult(this.defaultGlyphRanges); - - /// - /// Gets a value indicating whether game symbol font file is available. - /// - public bool HasGameSymbolsFontFile => - this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol); - - /// - public void Dispose() - { - this.cancellationTokenSource.Cancel(); - this.scopedFinalizer.Dispose(); - this.cancellationTokenSource.Dispose(); - } - - /// - /// Creates a new instance of a class that implements the interface. - /// - /// Name of atlas, for debugging and logging purposes. - /// Specify how to auto rebuild. - /// Whether the fonts in the atlas is global scaled. - /// The new font atlas. - public IFontAtlas CreateFontAtlas( - string atlasName, - FontAtlasAutoRebuildMode autoRebuildMode, - bool isGlobalScaled = true) => - new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled); - - /// - /// Adds the font from Dalamud Assets. - /// - /// The toolkitPostBuild. - /// The font. - /// The font config. - /// The address and size. - public ImFontPtr AddFont( - IFontAtlasBuildToolkitPreBuild toolkitPreBuild, - DalamudAsset asset, - in SafeFontConfig fontConfig) => - toolkitPreBuild.AddFontFromStream( - this.dalamudAssetManager.CreateStream(asset), - fontConfig, - false, - $"Asset({asset})"); - - /// - /// Gets the for the . - /// - /// The font family and size. - /// The . - public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); - - /// - public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) - { - var arr = ExtractResult(this.fdtFiles[gffas]); - var handle = arr.AsMemory().Pin(); - try - { - fdtFileView = new(handle.Pointer, arr.Length); - return handle; - } - catch - { - handle.Dispose(); - throw; - } - } - - /// - public int GetFontTextureCount(string texPathFormat) => - ExtractResult(this.prebakedTextureWraps[texPathFormat]).Length; - - /// - public TexFile GetTexFile(string texPathFormat, int index) => - ExtractResult(ExtractResult(this.texFiles[texPathFormat])[index]); - - /// - public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex) - { - lock (this.prebakedTextureWraps[texPathFormat]) - { - var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]); - var fileIndex = textureIndex / 4; - var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4]; - wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex); - return CloneTextureWrap(wraps[textureIndex]); - } - } - - private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); - - private static unsafe void ExtractChannelFromB8G8R8A8( - Span target, - ReadOnlySpan source, - int channelIndex, - bool targetIsB4G4R4A4) - { - var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); - - fixed (byte* sourcePtrImmutable = source) - { - var rptr = sourcePtrImmutable + channelIndex; - fixed (void* targetPtr = target) - { - if (targetIsB4G4R4A4) - { - var wptr = (ushort*)targetPtr; - while (numPixels-- > 0) - { - *wptr = (ushort)((*rptr << 8) | 0x0FFF); - wptr++; - rptr += 4; - } - } - else - { - var wptr = (uint*)targetPtr; - while (numPixels-- > 0) - { - *wptr = (uint)((*rptr << 24) | 0x00FFFFFF); - wptr++; - rptr += 4; - } - } - } - } - } - - /// - /// Clones a texture wrap, by getting a new reference to the underlying and the - /// texture behind. - /// - /// The to clone from. - /// The cloned . - private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) - { - var srv = CppObject.FromPointer(wrap.ImGuiHandle); - using var res = srv.Resource; - using var tex2D = res.QueryInterface(); - var description = tex2D.Description; - return new DalamudTextureWrap( - new D3DTextureWrap( - srv.QueryInterface(), - description.Width, - description.Height)); - } - - private static unsafe void ExtractChannelFromB4G4R4A4( - Span target, - ReadOnlySpan source, - int channelIndex, - bool targetIsB4G4R4A4) - { - var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); - fixed (byte* sourcePtrImmutable = source) - { - var rptr = sourcePtrImmutable + (channelIndex / 2); - var rshift = (channelIndex & 1) == 0 ? 0 : 4; - fixed (void* targetPtr = target) - { - if (targetIsB4G4R4A4) - { - var wptr = (ushort*)targetPtr; - while (numPixels-- > 0) - { - *wptr = (ushort)(((*rptr >> rshift) << 12) | 0x0FFF); - wptr++; - rptr += 2; - } - } - else - { - var wptr = (uint*)targetPtr; - while (numPixels-- > 0) - { - var v = (*rptr >> rshift) & 0xF; - v |= v << 4; - *wptr = (uint)((v << 24) | 0x00FFFFFF); - wptr++; - rptr += 4; - } - } - } - } - } - - private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) - { - var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); - var numPixels = texFile.Header.Width * texFile.Header.Height; - - _ = Service.Get(); - var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); - var bpp = targetIsB4G4R4A4 ? 2 : 4; - var buffer = ArrayPool.Shared.Rent(numPixels * bpp); - try - { - var sliceSpan = texFile.SliceSpan(0, 0, out _, out _, out _); - switch (texFile.Header.Format) - { - case TexFile.TextureFormat.B4G4R4A4: - // Game ships with this format. - ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); - break; - case TexFile.TextureFormat.B8G8R8A8: - // In case of modded font textures. - ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); - break; - default: - // Unlikely. - ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4); - break; - } - - return this.scopedFinalizer.Add( - this.InterfaceManager.LoadImageFromDxgiFormat( - buffer, - texFile.Header.Width * bpp, - texFile.Header.Width, - texFile.Header.Height, - targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)); - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs deleted file mode 100644 index 99c817a91..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ /dev/null @@ -1,857 +0,0 @@ -using System.Buffers; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reactive.Disposables; - -using Dalamud.Game.Text; -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Utility; - -using ImGuiNET; - -using Lumina.Data.Files; - -using Vector4 = System.Numerics.Vector4; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// A font handle that uses the game's built-in fonts, optionally with some styling. -/// -internal class GamePrebakedFontHandle : IFontHandle.IInternal -{ - /// - /// The smallest value of . - /// - public static readonly char SeIconCharMin = (char)Enum.GetValues().Min(); - - /// - /// The largest value of . - /// - public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); - - private IFontHandleManager? manager; - - /// - /// Initializes a new instance of the class. - /// - /// An instance of . - /// Font to use. - public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style) - { - if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined) - throw new ArgumentOutOfRangeException(nameof(style), style, null); - - if (style.SizePt <= 0) - throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style)); - - this.manager = manager; - this.FontStyle = style; - } - - /// - /// Provider for for `common/font/fontNN.tex`. - /// - public interface IGameFontTextureProvider - { - /// - /// Creates the for the .
- /// Dispose after use. - ///
- /// The font family and size. - /// The view. - /// Dispose this after use.. - public MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView); - - /// - /// Gets the number of font textures. - /// - /// Format of .tex path. - /// The number of textures. - public int GetFontTextureCount(string texPathFormat); - - /// - /// Gets the for the given index of a font. - /// - /// Format of .tex path. - /// The index of .tex file. - /// The . - public TexFile GetTexFile(string texPathFormat, int index); - - /// - /// Gets a new reference of the font texture. - /// - /// Format of .tex path. - /// Texture index. - /// The texture. - public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex); - } - - /// - /// Gets the font style. - /// - public GameFontStyle FontStyle { get; } - - /// - public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); - - /// - public bool Available => this.ImFont.IsNotNullAndLoaded(); - - /// - public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; - - private IFontHandleManager ManagerNotDisposed => - this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); - - /// - public void Dispose() - { - this.manager?.FreeFontHandle(this); - this.manager = null; - } - - /// - public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); - - /// - /// Manager for s. - /// - internal sealed class HandleManager : IFontHandleManager - { - private readonly Dictionary gameFontsRc = new(); - private readonly object syncRoot = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The name of the owner atlas. - /// An instance of . - public HandleManager(string atlasName, IGameFontTextureProvider gameFontTextureProvider) - { - this.GameFontTextureProvider = gameFontTextureProvider; - this.Name = $"{atlasName}:{nameof(GamePrebakedFontHandle)}:Manager"; - } - - /// - public event Action? RebuildRecommend; - - /// - public string Name { get; } - - /// - public IFontHandleSubstance? Substance { get; set; } - - /// - /// Gets an instance of . - /// - public IGameFontTextureProvider GameFontTextureProvider { get; } - - /// - public void Dispose() - { - this.Substance?.Dispose(); - this.Substance = null; - } - - /// - public IFontHandle NewFontHandle(GameFontStyle style) - { - var handle = new GamePrebakedFontHandle(this, style); - bool suggestRebuild; - lock (this.syncRoot) - { - this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; - suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; - } - - if (suggestRebuild) - this.RebuildRecommend?.Invoke(); - - return handle; - } - - /// - public void FreeFontHandle(IFontHandle handle) - { - if (handle is not GamePrebakedFontHandle ggfh) - return; - - lock (this.syncRoot) - { - if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) - return; - - if ((this.gameFontsRc[ggfh.FontStyle] -= 1) == 0) - this.gameFontsRc.Remove(ggfh.FontStyle); - } - } - - /// - public IFontHandleSubstance NewSubstance() - { - lock (this.syncRoot) - return new HandleSubstance(this, this.gameFontsRc.Keys); - } - } - - /// - /// Substance from . - /// - internal sealed class HandleSubstance : IFontHandleSubstance - { - private readonly HandleManager handleManager; - private readonly HashSet gameFontStyles; - - // Owned by this class, but ImFontPtr values still do not belong to this. - private readonly Dictionary fonts = new(); - private readonly Dictionary buildExceptions = new(); - private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new(); - - private readonly HashSet templatedFonts = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The manager. - /// The game font styles. - public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) - { - this.handleManager = manager; - Service.Get(); - this.gameFontStyles = new(gameFontStyles); - } - - /// - public IFontHandleManager Manager => this.handleManager; - - /// - public void Dispose() - { - } - - /// - /// Attaches game symbols to the given font. If font is null, it will be created. - /// - /// The toolkitPostBuild. - /// The font to attach to. - /// The game font style. - /// The intended glyph ranges. - /// if it is not empty; otherwise a new font. - public ImFontPtr AttachGameGlyphs( - IFontAtlasBuildToolkitPreBuild toolkitPreBuild, - ImFontPtr font, - GameFontStyle style, - ushort[]? glyphRanges = null) - { - if (font.IsNull()) - font = this.CreateTemplateFont(toolkitPreBuild, style.SizePx); - this.attachments.Add((font, style, glyphRanges)); - return font; - } - - /// - /// Creates or gets a relevant for the given . - /// - /// The game font style. - /// The toolkitPostBuild. - /// The font. - public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) - { - try - { - if (!this.fonts.TryGetValue(style, out var plan)) - { - plan = new( - style, - toolkitPreBuild.Scale, - this.handleManager.GameFontTextureProvider, - this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); - this.fonts[style] = plan; - } - - plan.AttachFont(plan.FullRangeFont); - return plan.FullRangeFont; - } - catch (Exception e) - { - this.buildExceptions[style] = e; - throw; - } - } - - /// - public ImFontPtr GetFontPtr(IFontHandle handle) => - handle is GamePrebakedFontHandle ggfh - ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default - : default; - - /// - public Exception? GetBuildException(IFontHandle handle) => - handle is GamePrebakedFontHandle ggfh ? this.buildExceptions.GetValueOrDefault(ggfh.FontStyle) : default; - - /// - public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) - { - foreach (var style in this.gameFontStyles) - { - if (this.fonts.ContainsKey(style)) - continue; - - try - { - _ = this.GetOrCreateFont(style, toolkitPreBuild); - } - catch - { - // ignore; it should have been recorded from the call - } - } - } - - /// - public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) - { - foreach (var (font, style, ranges) in this.attachments) - { - var effectiveStyle = - toolkitPreBuild.IsGlobalScaleIgnored(font) - ? style.Scale(1 / toolkitPreBuild.Scale) - : style; - if (!this.fonts.TryGetValue(style, out var plan)) - { - plan = new( - effectiveStyle, - toolkitPreBuild.Scale, - this.handleManager.GameFontTextureProvider, - this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); - this.fonts[style] = plan; - } - - plan.AttachFont(font, ranges); - } - - foreach (var plan in this.fonts.Values) - { - plan.EnsureGlyphs(toolkitPreBuild.NewImAtlas); - } - } - - /// - public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) - { - var allTextureIndices = new Dictionary(); - var allTexFiles = new Dictionary(); - using var rentReturn = Disposable.Create( - () => - { - foreach (var x in allTextureIndices.Values) - ArrayPool.Shared.Return(x); - foreach (var x in allTexFiles.Values) - ArrayPool.Shared.Return(x); - }); - - var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; - var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; - for (var i = 0; i < pixels8Array.Length; i++) - toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out _); - - foreach (var (style, plan) in this.fonts) - { - try - { - foreach (var font in plan.Ranges.Keys) - this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale); - - plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths); - plan.CopyGlyphsToRanges(toolkitPostBuild); - plan.PostProcessFullRangeFont(toolkitPostBuild.Scale); - } - catch (Exception e) - { - this.buildExceptions[style] = e; - this.fonts[style] = default; - } - } - } - - /// - public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) - { - // Irrelevant - } - - /// - /// Creates a new template font. - /// - /// The toolkitPostBuild. - /// The size of the font. - /// The font. - private ImFontPtr CreateTemplateFont(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, float sizePx) - { - var font = toolkitPreBuild.AddDalamudAssetFont( - DalamudAsset.NotoSansJpMedium, - new() - { - GlyphRanges = new ushort[] { ' ', ' ', '\0' }, - SizePx = sizePx, - }); - this.templatedFonts.Add(font); - return font; - } - - private unsafe void PatchFontMetricsIfNecessary(GameFontStyle style, ImFontPtr font, float atlasScale) - { - if (!this.templatedFonts.Contains(font)) - return; - - var fas = style.Scale(atlasScale).FamilyAndSize; - using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); - ref var fdtFontHeader = ref fdt.FontHeader; - var fontPtr = font.NativePtr; - - var scale = style.SizePt / fdtFontHeader.Size; - fontPtr->Ascent = fdtFontHeader.Ascent * scale; - fontPtr->Descent = fdtFontHeader.Descent * scale; - fontPtr->EllipsisChar = '…'; - } - } - - [SuppressMessage( - "StyleCop.CSharp.MaintainabilityRules", - "SA1401:Fields should be private", - Justification = "Internal")] - private sealed class FontDrawPlan : IDisposable - { - public readonly GameFontStyle Style; - public readonly GameFontStyle BaseStyle; - public readonly GameFontFamilyAndSizeAttribute BaseAttr; - public readonly int TexCount; - public readonly Dictionary Ranges = new(); - public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new(); - public readonly ushort[] RectLookup = new ushort[0x10000]; - public readonly FdtFileView Fdt; - public readonly ImFontPtr FullRangeFont; - - private readonly IDisposable fdtHandle; - private readonly IGameFontTextureProvider gftp; - - public FontDrawPlan( - GameFontStyle style, - float scale, - IGameFontTextureProvider gameFontTextureProvider, - ImFontPtr fullRangeFont) - { - this.Style = style; - this.BaseStyle = style.Scale(scale); - this.BaseAttr = this.BaseStyle.FamilyAndSize.GetAttribute()!; - this.gftp = gameFontTextureProvider; - this.TexCount = this.gftp.GetFontTextureCount(this.BaseAttr.TexPathFormat); - this.fdtHandle = this.gftp.CreateFdtFileView(this.BaseStyle.FamilyAndSize, out this.Fdt); - this.RectLookup.AsSpan().Fill(ushort.MaxValue); - this.FullRangeFont = fullRangeFont; - this.Ranges[fullRangeFont] = new(0x10000); - } - - public void Dispose() - { - this.fdtHandle.Dispose(); - } - - public void AttachFont(ImFontPtr font, ushort[]? glyphRanges = null) - { - if (!this.Ranges.TryGetValue(font, out var rangeBitArray)) - rangeBitArray = this.Ranges[font] = new(0x10000); - - if (glyphRanges is null) - { - foreach (ref var g in this.Fdt.Glyphs) - { - var c = g.CharInt; - if (c is >= 0x20 and <= 0xFFFE) - rangeBitArray[c] = true; - } - - return; - } - - for (var i = 0; i < glyphRanges.Length - 1; i += 2) - { - if (glyphRanges[i] == 0) - break; - var from = (int)glyphRanges[i]; - var to = (int)glyphRanges[i + 1]; - for (var j = from; j <= to; j++) - rangeBitArray[j] = true; - } - } - - public unsafe void EnsureGlyphs(ImFontAtlasPtr atlas) - { - var glyphs = this.Fdt.Glyphs; - var ranges = this.Ranges[this.FullRangeFont]; - foreach (var (font, extraRange) in this.Ranges) - { - if (font.NativePtr != this.FullRangeFont.NativePtr) - ranges.Or(extraRange); - } - - if (this.Style is not { Weight: 0, SkewStrength: 0 }) - { - for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) - { - ref var glyph = ref glyphs[fdtGlyphIndex]; - var cint = glyph.CharInt; - if (cint > char.MaxValue) - continue; - if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) - continue; - - var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(this.Fdt.FontHeader, glyph); - this.RectLookup[cint] = (ushort)this.Rects.Count; - this.Rects.Add( - ( - atlas.AddCustomRectFontGlyph( - this.FullRangeFont, - (char)cint, - glyph.BoundingWidth + widthAdjustment, - glyph.BoundingHeight, - glyph.AdvanceWidth, - new(this.BaseAttr.HorizontalOffset, glyph.CurrentOffsetY)), - fdtGlyphIndex)); - } - } - else - { - for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) - { - ref var glyph = ref glyphs[fdtGlyphIndex]; - var cint = glyph.CharInt; - if (cint > char.MaxValue) - continue; - if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) - continue; - - this.RectLookup[cint] = (ushort)this.Rects.Count; - this.Rects.Add((-1, fdtGlyphIndex)); - } - } - } - - public unsafe void PostProcessFullRangeFont(float atlasScale) - { - var round = 1 / atlasScale; - var pfrf = this.FullRangeFont.NativePtr; - ref var frf = ref *pfrf; - - frf.FontSize = MathF.Round(frf.FontSize / round) * round; - frf.Ascent = MathF.Round(frf.Ascent / round) * round; - frf.Descent = MathF.Round(frf.Descent / round) * round; - - var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; - foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan) - { - var w = (g.X1 - g.X0) * scale; - var h = (g.Y1 - g.Y0) * scale; - g.X0 = MathF.Round((g.X0 * scale) / round) * round; - g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; - g.X1 = g.X0 + w; - g.Y1 = g.Y0 + h; - g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; - } - - var fullRange = this.Ranges[this.FullRangeFont]; - foreach (ref var k in this.Fdt.PairAdjustments) - { - var (leftInt, rightInt) = (k.LeftInt, k.RightInt); - if (leftInt > char.MaxValue || rightInt > char.MaxValue) - continue; - if (!fullRange[leftInt] || !fullRange[rightInt]) - continue; - ImGuiNative.ImFont_AddKerningPair( - pfrf, - (ushort)leftInt, - (ushort)rightInt, - MathF.Round((k.RightOffset * scale) / round) * round); - } - - pfrf->FallbackGlyph = null; - ImGuiNative.ImFont_BuildLookupTable(pfrf); - - foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) - { - var glyph = ImGuiNative.ImFont_FindGlyphNoFallback(pfrf, fallbackCharCandidate); - if ((nint)glyph == IntPtr.Zero) - continue; - frf.FallbackChar = fallbackCharCandidate; - frf.FallbackGlyph = glyph; - frf.FallbackHotData = - (ImFontGlyphHotData*)frf.IndexedHotData.Address( - fallbackCharCandidate); - break; - } - } - - public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) - { - var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; - var atlasScale = toolkitPostBuild.Scale; - var round = 1 / atlasScale; - - foreach (var (font, rangeBits) in this.Ranges) - { - if (font.NativePtr == this.FullRangeFont.NativePtr) - continue; - - var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font); - - var lookup = font.IndexLookupWrapped(); - var glyphs = font.GlyphsWrapped(); - foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan) - { - if (!rangeBits[sourceGlyph.Codepoint]) - continue; - - var glyphIndex = ushort.MaxValue; - if (sourceGlyph.Codepoint < lookup.Length) - glyphIndex = lookup[sourceGlyph.Codepoint]; - - if (glyphIndex == ushort.MaxValue) - { - glyphIndex = (ushort)glyphs.Length; - glyphs.Add(default); - } - - ref var g = ref glyphs[glyphIndex]; - g = sourceGlyph; - if (noGlobalScale) - { - g.XY *= scale; - g.AdvanceX *= scale; - } - else - { - var w = (g.X1 - g.X0) * scale; - var h = (g.Y1 - g.Y0) * scale; - g.X0 = MathF.Round((g.X0 * scale) / round) * round; - g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; - g.X1 = g.X0 + w; - g.Y1 = g.Y0 + h; - g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; - } - } - - foreach (ref var k in this.Fdt.PairAdjustments) - { - var (leftInt, rightInt) = (k.LeftInt, k.RightInt); - if (leftInt > char.MaxValue || rightInt > char.MaxValue) - continue; - if (!rangeBits[leftInt] || !rangeBits[rightInt]) - continue; - if (noGlobalScale) - { - font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale); - } - else - { - font.AddKerningPair( - (ushort)leftInt, - (ushort)rightInt, - MathF.Round((k.RightOffset * scale) / round) * round); - } - } - - font.NativePtr->FallbackGlyph = null; - font.BuildLookupTable(); - - foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) - { - var glyph = font.FindGlyphNoFallback(fallbackCharCandidate).NativePtr; - if ((nint)glyph == IntPtr.Zero) - continue; - - ref var frf = ref *font.NativePtr; - frf.FallbackChar = fallbackCharCandidate; - frf.FallbackGlyph = glyph; - frf.FallbackHotData = - (ImFontGlyphHotData*)frf.IndexedHotData.Address( - fallbackCharCandidate); - break; - } - } - } - - public unsafe void SetFullRangeFontGlyphs( - IFontAtlasBuildToolkitPostBuild toolkitPostBuild, - Dictionary allTexFiles, - Dictionary allTextureIndices, - byte*[] pixels8Array, - int[] widths) - { - var glyphs = this.FullRangeFont.GlyphsWrapped(); - var lookups = this.FullRangeFont.IndexLookupWrapped(); - - ref var fdtFontHeader = ref this.Fdt.FontHeader; - var fdtGlyphs = this.Fdt.Glyphs; - var fdtTexSize = new Vector4( - this.Fdt.FontHeader.TextureWidth, - this.Fdt.FontHeader.TextureHeight, - this.Fdt.FontHeader.TextureWidth, - this.Fdt.FontHeader.TextureHeight); - - if (!allTexFiles.TryGetValue(this.BaseAttr.TexPathFormat, out var texFiles)) - { - allTexFiles.Add( - this.BaseAttr.TexPathFormat, - texFiles = ArrayPool.Shared.Rent(this.TexCount)); - } - - if (!allTextureIndices.TryGetValue(this.BaseAttr.TexPathFormat, out var textureIndices)) - { - allTextureIndices.Add( - this.BaseAttr.TexPathFormat, - textureIndices = ArrayPool.Shared.Rent(this.TexCount)); - textureIndices.AsSpan(0, this.TexCount).Fill(-1); - } - - var pixelWidth = Math.Max(1, (int)MathF.Ceiling(this.BaseStyle.Weight + 1)); - var pixelStrength = stackalloc byte[pixelWidth]; - for (var i = 0; i < pixelWidth; i++) - pixelStrength[i] = (byte)(255 * Math.Min(1f, (this.BaseStyle.Weight + 1) - i)); - - var minGlyphY = 0; - var maxGlyphY = 0; - foreach (ref var g in fdtGlyphs) - { - minGlyphY = Math.Min(g.CurrentOffsetY, minGlyphY); - maxGlyphY = Math.Max(g.BoundingHeight + g.CurrentOffsetY, maxGlyphY); - } - - var horzShift = stackalloc int[maxGlyphY - minGlyphY]; - var horzBlend = stackalloc byte[maxGlyphY - minGlyphY]; - horzShift -= minGlyphY; - horzBlend -= minGlyphY; - if (this.BaseStyle.BaseSkewStrength != 0) - { - for (var i = minGlyphY; i < maxGlyphY; i++) - { - float blend = this.BaseStyle.BaseSkewStrength switch - { - > 0 => fdtFontHeader.LineHeight - i, - < 0 => -i, - _ => throw new InvalidOperationException(), - }; - blend *= this.BaseStyle.BaseSkewStrength / fdtFontHeader.LineHeight; - horzShift[i] = (int)MathF.Floor(blend); - horzBlend[i] = (byte)(255 * (blend - horzShift[i])); - } - } - - foreach (var (rectId, fdtGlyphIndex) in this.Rects) - { - ref var fdtGlyph = ref fdtGlyphs[fdtGlyphIndex]; - if (rectId == -1) - { - ref var textureIndex = ref textureIndices[fdtGlyph.TextureIndex]; - if (textureIndex == -1) - { - textureIndex = toolkitPostBuild.StoreTexture( - this.gftp.NewFontTextureRef(this.BaseAttr.TexPathFormat, fdtGlyph.TextureIndex), - true); - } - - var glyph = new ImGuiHelpers.ImFontGlyphReal - { - AdvanceX = fdtGlyph.AdvanceWidth, - Codepoint = fdtGlyph.Char, - Colored = false, - TextureIndex = textureIndex, - Visible = true, - X0 = this.BaseAttr.HorizontalOffset, - Y0 = fdtGlyph.CurrentOffsetY, - U0 = fdtGlyph.TextureOffsetX, - V0 = fdtGlyph.TextureOffsetY, - U1 = fdtGlyph.BoundingWidth, - V1 = fdtGlyph.BoundingHeight, - }; - - glyph.XY1 = glyph.XY0 + glyph.UV1; - glyph.UV1 += glyph.UV0; - glyph.UV /= fdtTexSize; - - glyphs.Add(glyph); - } - else - { - ref var rc = ref *(ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas - .GetCustomRectByIndex(rectId) - .NativePtr; - var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(fdtFontHeader, fdtGlyph); - - // Glyph is scaled at this point; undo that. - ref var glyph = ref glyphs[lookups[rc.GlyphId]]; - glyph.X0 = this.BaseAttr.HorizontalOffset; - glyph.Y0 = fdtGlyph.CurrentOffsetY; - glyph.X1 = glyph.X0 + fdtGlyph.BoundingWidth + widthAdjustment; - glyph.Y1 = glyph.Y0 + fdtGlyph.BoundingHeight; - glyph.AdvanceX = fdtGlyph.AdvanceWidth; - - var pixels8 = pixels8Array[rc.TextureIndex]; - var width = widths[rc.TextureIndex]; - texFiles[fdtGlyph.TextureFileIndex] ??= - this.gftp.GetTexFile(this.BaseAttr.TexPathFormat, fdtGlyph.TextureFileIndex); - var sourceBuffer = texFiles[fdtGlyph.TextureFileIndex].ImageData; - var sourceBufferDelta = fdtGlyph.TextureChannelByteIndex; - - for (var y = 0; y < fdtGlyph.BoundingHeight; y++) - { - var sourcePixelIndex = - ((fdtGlyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + fdtGlyph.TextureOffsetX; - sourcePixelIndex *= 4; - sourcePixelIndex += sourceBufferDelta; - var blend1 = horzBlend[fdtGlyph.CurrentOffsetY + y]; - - var targetOffset = ((rc.Y + y) * width) + rc.X; - for (var x = 0; x < rc.Width; x++) - pixels8[targetOffset + x] = 0; - - targetOffset += horzShift[fdtGlyph.CurrentOffsetY + y]; - if (blend1 == 0) - { - for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) - { - var n = sourceBuffer[sourcePixelIndex + 4]; - for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) - { - ref var p = ref pixels8[targetOffset + boldOffset]; - p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255)); - } - } - } - else - { - var blend2 = 255 - blend1; - for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) - { - var a1 = sourceBuffer[sourcePixelIndex]; - var a2 = x == fdtGlyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourcePixelIndex + 4]; - var n = (a1 * blend1) + (a2 * blend2); - - for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) - { - ref var p = ref pixels8[targetOffset + boldOffset]; - p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255 / 255)); - } - } - } - } - } - } - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs deleted file mode 100644 index 93c688608..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Manager for . -/// -internal interface IFontHandleManager : IDisposable -{ - /// - event Action? RebuildRecommend; - - /// - /// Gets the name of the font handle manager. For logging and debugging purposes. - /// - string Name { get; } - - /// - /// Gets or sets the active font handle substance. - /// - IFontHandleSubstance? Substance { get; set; } - - /// - /// Decrease font reference counter. - /// - /// Handle being released. - void FreeFontHandle(IFontHandle handle); - - /// - /// Creates a new substance of the font atlas. - /// - /// The new substance. - IFontHandleSubstance NewSubstance(); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs deleted file mode 100644 index f6c5c6591..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ /dev/null @@ -1,54 +0,0 @@ -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Substance of a font. -/// -internal interface IFontHandleSubstance : IDisposable -{ - /// - /// Gets the manager relevant to this instance of . - /// - IFontHandleManager Manager { get; } - - /// - /// Gets the font. - /// - /// The handle to get from. - /// Corresponding font or null. - ImFontPtr GetFontPtr(IFontHandle handle); - - /// - /// Gets the exception happened while loading for the font. - /// - /// The handle to get from. - /// Corresponding font or null. - Exception? GetBuildException(IFontHandle handle); - - /// - /// Called before call. - /// - /// The toolkit. - void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); - - /// - /// Called between and calls.
- /// Any further modification to will result in undefined behavior. - ///
- /// The toolkit. - void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); - - /// - /// Called after call. - /// - /// The toolkit. - void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild); - - /// - /// Called on the specific thread depending on after - /// promoting the staging atlas to direct use with . - /// - /// The toolkit. - void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs deleted file mode 100644 index 8e7149853..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System.Buffers.Binary; -using System.Runtime.InteropServices; -using System.Text; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -internal static partial class TrueTypeUtils -{ - private struct Fixed : IComparable - { - public ushort Major; - public ushort Minor; - - public Fixed(ushort major, ushort minor) - { - this.Major = major; - this.Minor = minor; - } - - public Fixed(PointerSpan span) - { - var offset = 0; - span.ReadBig(ref offset, out this.Major); - span.ReadBig(ref offset, out this.Minor); - } - - public int CompareTo(Fixed other) - { - var majorComparison = this.Major.CompareTo(other.Major); - return majorComparison != 0 ? majorComparison : this.Minor.CompareTo(other.Minor); - } - } - - private struct KerningPair : IEquatable - { - public ushort Left; - public ushort Right; - public short Value; - - public KerningPair(PointerSpan span) - { - var offset = 0; - span.ReadBig(ref offset, out this.Left); - span.ReadBig(ref offset, out this.Right); - span.ReadBig(ref offset, out this.Value); - } - - public KerningPair(ushort left, ushort right, short value) - { - this.Left = left; - this.Right = right; - this.Value = value; - } - - public static bool operator ==(KerningPair left, KerningPair right) => left.Equals(right); - - public static bool operator !=(KerningPair left, KerningPair right) => !left.Equals(right); - - public static KerningPair ReverseEndianness(KerningPair pair) => new() - { - Left = BinaryPrimitives.ReverseEndianness(pair.Left), - Right = BinaryPrimitives.ReverseEndianness(pair.Right), - Value = BinaryPrimitives.ReverseEndianness(pair.Value), - }; - - public bool Equals(KerningPair other) => - this.Left == other.Left && this.Right == other.Right && this.Value == other.Value; - - public override bool Equals(object? obj) => obj is KerningPair other && this.Equals(other); - - public override int GetHashCode() => HashCode.Combine(this.Left, this.Right, this.Value); - - public override string ToString() => $"KerningPair[{this.Left}, {this.Right}] = {this.Value}"; - } - - [StructLayout(LayoutKind.Explicit, Size = 4)] - private struct PlatformAndEncoding - { - [FieldOffset(0)] - public PlatformId Platform; - - [FieldOffset(2)] - public UnicodeEncodingId UnicodeEncoding; - - [FieldOffset(2)] - public MacintoshEncodingId MacintoshEncoding; - - [FieldOffset(2)] - public IsoEncodingId IsoEncoding; - - [FieldOffset(2)] - public WindowsEncodingId WindowsEncoding; - - public PlatformAndEncoding(PointerSpan source) - { - var offset = 0; - source.ReadBig(ref offset, out this.Platform); - source.ReadBig(ref offset, out this.UnicodeEncoding); - } - - public static PlatformAndEncoding ReverseEndianness(PlatformAndEncoding value) => new() - { - Platform = (PlatformId)BinaryPrimitives.ReverseEndianness((ushort)value.Platform), - UnicodeEncoding = (UnicodeEncodingId)BinaryPrimitives.ReverseEndianness((ushort)value.UnicodeEncoding), - }; - - public readonly string Decode(Span data) - { - switch (this.Platform) - { - case PlatformId.Unicode: - switch (this.UnicodeEncoding) - { - case UnicodeEncodingId.Unicode_2_0_Bmp: - case UnicodeEncodingId.Unicode_2_0_Full: - return Encoding.BigEndianUnicode.GetString(data); - } - - break; - - case PlatformId.Macintosh: - switch (this.MacintoshEncoding) - { - case MacintoshEncodingId.Roman: - return Encoding.ASCII.GetString(data); - } - - break; - - case PlatformId.Windows: - switch (this.WindowsEncoding) - { - case WindowsEncodingId.Symbol: - case WindowsEncodingId.UnicodeBmp: - case WindowsEncodingId.UnicodeFullRepertoire: - return Encoding.BigEndianUnicode.GetString(data); - } - - break; - } - - throw new NotSupportedException(); - } - } - - [StructLayout(LayoutKind.Explicit)] - private struct TagStruct : IEquatable, IComparable - { - [FieldOffset(0)] - public unsafe fixed byte Tag[4]; - - [FieldOffset(0)] - public uint NativeValue; - - public unsafe TagStruct(char c1, char c2, char c3, char c4) - { - this.Tag[0] = checked((byte)c1); - this.Tag[1] = checked((byte)c2); - this.Tag[2] = checked((byte)c3); - this.Tag[3] = checked((byte)c4); - } - - public unsafe TagStruct(PointerSpan span) - { - this.Tag[0] = span[0]; - this.Tag[1] = span[1]; - this.Tag[2] = span[2]; - this.Tag[3] = span[3]; - } - - public unsafe TagStruct(ReadOnlySpan span) - { - this.Tag[0] = span[0]; - this.Tag[1] = span[1]; - this.Tag[2] = span[2]; - this.Tag[3] = span[3]; - } - - public unsafe byte this[int index] - { - get => this.Tag[index]; - set => this.Tag[index] = value; - } - - public static bool operator ==(TagStruct left, TagStruct right) => left.Equals(right); - - public static bool operator !=(TagStruct left, TagStruct right) => !left.Equals(right); - - public bool Equals(TagStruct other) => this.NativeValue == other.NativeValue; - - public override bool Equals(object? obj) => obj is TagStruct other && this.Equals(other); - - public override int GetHashCode() => (int)this.NativeValue; - - public int CompareTo(TagStruct other) => this.NativeValue.CompareTo(other.NativeValue); - - public override unsafe string ToString() => - $"0x{this.NativeValue:08X} \"{(char)this.Tag[0]}{(char)this.Tag[1]}{(char)this.Tag[2]}{(char)this.Tag[3]}\""; - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs deleted file mode 100644 index f6a653a51..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -internal static partial class TrueTypeUtils -{ - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] - private enum IsoEncodingId : ushort - { - Ascii = 0, - Iso_10646 = 1, - Iso_8859_1 = 2, - } - - private enum MacintoshEncodingId : ushort - { - Roman = 0, - } - - private enum NameId : ushort - { - CopyrightNotice = 0, - FamilyName = 1, - SubfamilyName = 2, - UniqueId = 3, - FullFontName = 4, - VersionString = 5, - PostScriptName = 6, - Trademark = 7, - Manufacturer = 8, - Designer = 9, - Description = 10, - UrlVendor = 11, - UrlDesigner = 12, - LicenseDescription = 13, - LicenseInfoUrl = 14, - TypographicFamilyName = 16, - TypographicSubfamilyName = 17, - CompatibleFullMac = 18, - SampleText = 19, - PoscSriptCidFindFontName = 20, - WwsFamilyName = 21, - WwsSubfamilyName = 22, - LightBackgroundPalette = 23, - DarkBackgroundPalette = 24, - VariationPostScriptNamePrefix = 25, - } - - private enum PlatformId : ushort - { - Unicode = 0, - Macintosh = 1, // discouraged - Iso = 2, // deprecated - Windows = 3, - Custom = 4, // OTF Windows NT compatibility mapping - } - - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] - private enum UnicodeEncodingId : ushort - { - Unicode_1_0 = 0, // deprecated - Unicode_1_1 = 1, // deprecated - IsoIec_10646 = 2, // deprecated - Unicode_2_0_Bmp = 3, - Unicode_2_0_Full = 4, - UnicodeVariationSequences = 5, - UnicodeFullRepertoire = 6, - } - - private enum WindowsEncodingId : ushort - { - Symbol = 0, - UnicodeBmp = 1, - ShiftJis = 2, - Prc = 3, - Big5 = 4, - Wansung = 5, - Johab = 6, - UnicodeFullRepertoire = 10, - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs deleted file mode 100644 index 3d89dd806..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] -[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] -[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] -[SuppressMessage( - "StyleCop.CSharp.NamingRules", - "SA1310:Field names should not contain underscore", - Justification = "Version name")] -[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name")] -internal static partial class TrueTypeUtils -{ - private readonly struct SfntFile : IReadOnlyDictionary> - { - // http://formats.kaitai.io/ttf/ttf.svg - - public static readonly TagStruct FileTagTrueType1 = new('1', '\0', '\0', '\0'); - public static readonly TagStruct FileTagType1 = new('t', 'y', 'p', '1'); - public static readonly TagStruct FileTagOpenTypeWithCff = new('O', 'T', 'T', 'O'); - public static readonly TagStruct FileTagOpenType1_0 = new('\0', '\x01', '\0', '\0'); - public static readonly TagStruct FileTagTrueTypeApple = new('t', 'r', 'u', 'e'); - - public readonly PointerSpan Memory; - public readonly int OffsetInCollection; - public readonly ushort TableCount; - - public SfntFile(PointerSpan memory, int offsetInCollection = 0) - { - var span = memory.Span; - this.Memory = memory; - this.OffsetInCollection = offsetInCollection; - this.TableCount = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); - } - - public int Count => this.TableCount; - - public IEnumerable Keys => this.Select(x => x.Key); - - public IEnumerable> Values => this.Select(x => x.Value); - - public PointerSpan this[TagStruct key] => this.First(x => x.Key == key).Value; - - public IEnumerator>> GetEnumerator() - { - var offset = 12; - for (var i = 0; i < this.TableCount; i++) - { - var dte = new DirectoryTableEntry(this.Memory[offset..]); - yield return new(dte.Tag, this.Memory.Slice(dte.Offset - this.OffsetInCollection, dte.Length)); - - offset += Unsafe.SizeOf(); - } - } - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - public bool ContainsKey(TagStruct key) => this.Any(x => x.Key == key); - - public bool TryGetValue(TagStruct key, out PointerSpan value) - { - foreach (var (k, v) in this) - { - if (k == key) - { - value = v; - return true; - } - } - - value = default; - return false; - } - - public readonly struct DirectoryTableEntry - { - public readonly PointerSpan Memory; - - public DirectoryTableEntry(PointerSpan span) => this.Memory = span; - - public TagStruct Tag => new(this.Memory); - - public uint Checksum => this.Memory.ReadU32Big(4); - - public int Offset => this.Memory.ReadI32Big(8); - - public int Length => this.Memory.ReadI32Big(12); - } - } - - private readonly struct TtcFile : IReadOnlyList - { - public static readonly TagStruct FileTag = new('t', 't', 'c', 'f'); - - public readonly PointerSpan Memory; - public readonly TagStruct Tag; - public readonly ushort MajorVersion; - public readonly ushort MinorVersion; - public readonly int FontCount; - - public TtcFile(PointerSpan memory) - { - var span = memory.Span; - this.Memory = memory; - this.Tag = new(span); - if (this.Tag != FileTag) - throw new InvalidOperationException(); - - this.MajorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); - this.MinorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[6..]); - this.FontCount = BinaryPrimitives.ReadInt32BigEndian(span[8..]); - } - - public int Count => this.FontCount; - - public SfntFile this[int index] - { - get - { - if (index < 0 || index >= this.FontCount) - { - throw new IndexOutOfRangeException( - $"The requested font #{index} does not exist in this .ttc file."); - } - - var offset = BinaryPrimitives.ReadInt32BigEndian(this.Memory.Span[(12 + 4 * index)..]); - return new(this.Memory[offset..], offset); - } - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < this.FontCount; i++) - yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs deleted file mode 100644 index d200de47b..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -internal static partial class TrueTypeUtils -{ - [Flags] - private enum LookupFlags : byte - { - RightToLeft = 1 << 0, - IgnoreBaseGlyphs = 1 << 1, - IgnoreLigatures = 1 << 2, - IgnoreMarks = 1 << 3, - UseMarkFilteringSet = 1 << 4, - } - - private enum LookupType : ushort - { - SingleAdjustment = 1, - PairAdjustment = 2, - CursiveAttachment = 3, - MarkToBaseAttachment = 4, - MarkToLigatureAttachment = 5, - MarkToMarkAttachment = 6, - ContextPositioning = 7, - ChainedContextPositioning = 8, - ExtensionPositioning = 9, - } - - private readonly struct ClassDefTable - { - public readonly PointerSpan Memory; - - public ClassDefTable(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public Format1ClassArray Format1 => new(this.Memory); - - public Format2ClassRanges Format2 => new(this.Memory); - - public IEnumerable<(ushort Class, ushort GlyphId)> Enumerate() - { - switch (this.Format) - { - case 1: - { - var format1 = this.Format1; - var startId = format1.StartGlyphId; - var count = format1.GlyphCount; - var classes = format1.ClassValueArray; - for (var i = 0; i < count; i++) - yield return (classes[i], (ushort)(i + startId)); - - break; - } - - case 2: - { - foreach (var range in this.Format2.ClassValueArray) - { - var @class = range.Class; - var startId = range.StartGlyphId; - var count = range.EndGlyphId - startId + 1; - for (var i = 0; i < count; i++) - yield return (@class, (ushort)(startId + i)); - } - - break; - } - } - } - - [Pure] - public ushort GetClass(ushort glyphId) - { - switch (this.Format) - { - case 1: - { - var format1 = this.Format1; - var startId = format1.StartGlyphId; - if (startId <= glyphId && glyphId < startId + format1.GlyphCount) - return this.Format1.ClassValueArray[glyphId - startId]; - - break; - } - - case 2: - { - var rangeSpan = this.Format2.ClassValueArray; - var i = rangeSpan.BinarySearch(new Format2ClassRanges.ClassRangeRecord { EndGlyphId = glyphId }); - if (i >= 0 && rangeSpan[i].ContainsGlyph(glyphId)) - return rangeSpan[i].Class; - - break; - } - } - - return 0; - } - - public readonly struct Format1ClassArray - { - public readonly PointerSpan Memory; - - public Format1ClassArray(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort StartGlyphId => this.Memory.ReadU16Big(2); - - public ushort GlyphCount => this.Memory.ReadU16Big(4); - - public BigEndianPointerSpan ClassValueArray => new( - this.Memory[6..].As(this.GlyphCount), - BinaryPrimitives.ReverseEndianness); - } - - public readonly struct Format2ClassRanges - { - public readonly PointerSpan Memory; - - public Format2ClassRanges(PointerSpan memory) => this.Memory = memory; - - public ushort ClassRangeCount => this.Memory.ReadU16Big(2); - - public BigEndianPointerSpan ClassValueArray => new( - this.Memory[4..].As(this.ClassRangeCount), - ClassRangeRecord.ReverseEndianness); - - public struct ClassRangeRecord : IComparable - { - public ushort StartGlyphId; - public ushort EndGlyphId; - public ushort Class; - - public static ClassRangeRecord ReverseEndianness(ClassRangeRecord value) => new() - { - StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), - EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), - Class = BinaryPrimitives.ReverseEndianness(value.Class), - }; - - public int CompareTo(ClassRangeRecord other) => this.EndGlyphId.CompareTo(other.EndGlyphId); - - public bool ContainsGlyph(ushort glyphId) => - this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; - } - } - } - - private readonly struct CoverageTable - { - public readonly PointerSpan Memory; - - public CoverageTable(PointerSpan memory) => this.Memory = memory; - - public enum CoverageFormat : ushort - { - Glyphs = 1, - RangeRecords = 2, - } - - public CoverageFormat Format => this.Memory.ReadEnumBig(0); - - public ushort Count => this.Memory.ReadU16Big(2); - - public BigEndianPointerSpan Glyphs => - this.Format == CoverageFormat.Glyphs - ? new(this.Memory[4..].As(this.Count), BinaryPrimitives.ReverseEndianness) - : default(BigEndianPointerSpan); - - public BigEndianPointerSpan RangeRecords => - this.Format == CoverageFormat.RangeRecords - ? new(this.Memory[4..].As(this.Count), RangeRecord.ReverseEndianness) - : default(BigEndianPointerSpan); - - public int GetCoverageIndex(ushort glyphId) - { - switch (this.Format) - { - case CoverageFormat.Glyphs: - return this.Glyphs.BinarySearch(glyphId); - - case CoverageFormat.RangeRecords: - { - var index = this.RangeRecords.BinarySearch( - (in RangeRecord record) => glyphId.CompareTo(record.EndGlyphId)); - - if (index >= 0 && this.RangeRecords[index].ContainsGlyph(glyphId)) - return index; - - return -1; - } - - default: - return -1; - } - } - - public struct RangeRecord - { - public ushort StartGlyphId; - public ushort EndGlyphId; - public ushort StartCoverageIndex; - - public static RangeRecord ReverseEndianness(RangeRecord value) => new() - { - StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), - EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), - StartCoverageIndex = BinaryPrimitives.ReverseEndianness(value.StartCoverageIndex), - }; - - public bool ContainsGlyph(ushort glyphId) => - this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; - } - } - - private readonly struct LookupTable : IEnumerable> - { - public readonly PointerSpan Memory; - - public LookupTable(PointerSpan memory) => this.Memory = memory; - - public LookupType Type => this.Memory.ReadEnumBig(0); - - public byte MarkAttachmentType => this.Memory[2]; - - public LookupFlags Flags => (LookupFlags)this.Memory[3]; - - public ushort SubtableCount => this.Memory.ReadU16Big(4); - - public BigEndianPointerSpan SubtableOffsets => new( - this.Memory[6..].As(this.SubtableCount), - BinaryPrimitives.ReverseEndianness); - - public PointerSpan this[int index] => this.Memory[this.SubtableOffsets[this.EnsureIndex(index)] ..]; - - public IEnumerator> GetEnumerator() - { - foreach (var i in Enumerable.Range(0, this.SubtableCount)) - yield return this.Memory[this.SubtableOffsets[i] ..]; - } - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - private int EnsureIndex(int index) => index >= 0 && index < this.SubtableCount - ? index - : throw new IndexOutOfRangeException(); - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs deleted file mode 100644 index c91df4ff2..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs +++ /dev/null @@ -1,443 +0,0 @@ -using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -internal static partial class TrueTypeUtils -{ - private delegate int BinarySearchComparer(in T value); - - private static IDisposable CreatePointerSpan(this T[] data, out PointerSpan pointerSpan) - where T : unmanaged - { - var gchandle = GCHandle.Alloc(data, GCHandleType.Pinned); - pointerSpan = new(gchandle.AddrOfPinnedObject(), data.Length); - return Disposable.Create(() => gchandle.Free()); - } - - private static int BinarySearch(this IReadOnlyList span, in T value) - where T : unmanaged, IComparable - { - var l = 0; - var r = span.Count - 1; - while (l <= r) - { - var i = (int)(((uint)r + (uint)l) >> 1); - var c = value.CompareTo(span[i]); - switch (c) - { - case 0: - return i; - case > 0: - l = i + 1; - break; - default: - r = i - 1; - break; - } - } - - return ~l; - } - - private static int BinarySearch(this IReadOnlyList span, BinarySearchComparer comparer) - where T : unmanaged - { - var l = 0; - var r = span.Count - 1; - while (l <= r) - { - var i = (int)(((uint)r + (uint)l) >> 1); - var c = comparer(span[i]); - switch (c) - { - case 0: - return i; - case > 0: - l = i + 1; - break; - default: - r = i - 1; - break; - } - } - - return ~l; - } - - private static short ReadI16Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); - - private static int ReadI32Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); - - private static long ReadI64Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); - - private static ushort ReadU16Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); - - private static uint ReadU32Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); - - private static ulong ReadU64Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); - - private static Half ReadF16Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); - - private static float ReadF32Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); - - private static double ReadF64Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out short value) => - value = BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out int value) => - value = BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out long value) => - value = BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out ushort value) => - value = BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out uint value) => - value = BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out ulong value) => - value = BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out Half value) => - value = BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out float value) => - value = BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out double value) => - value = BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, ref int offset, out short value) - { - ps.ReadBig(offset, out value); - offset += 2; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out int value) - { - ps.ReadBig(offset, out value); - offset += 4; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out long value) - { - ps.ReadBig(offset, out value); - offset += 8; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out ushort value) - { - ps.ReadBig(offset, out value); - offset += 2; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out uint value) - { - ps.ReadBig(offset, out value); - offset += 4; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out ulong value) - { - ps.ReadBig(offset, out value); - offset += 8; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out Half value) - { - ps.ReadBig(offset, out value); - offset += 2; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out float value) - { - ps.ReadBig(offset, out value); - offset += 4; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out double value) - { - ps.ReadBig(offset, out value); - offset += 8; - } - - private static unsafe T ReadEnumBig(this PointerSpan ps, int offset) where T : unmanaged, Enum - { - switch (Marshal.SizeOf(Enum.GetUnderlyingType(typeof(T)))) - { - case 1: - var b1 = ps.Span[offset]; - return *(T*)&b1; - case 2: - var b2 = ps.ReadU16Big(offset); - return *(T*)&b2; - case 4: - var b4 = ps.ReadU32Big(offset); - return *(T*)&b4; - case 8: - var b8 = ps.ReadU64Big(offset); - return *(T*)&b8; - default: - throw new ArgumentException("Enum is not of size 1, 2, 4, or 8.", nameof(T), null); - } - } - - private static void ReadBig(this PointerSpan ps, int offset, out T value) where T : unmanaged, Enum => - value = ps.ReadEnumBig(offset); - - private static void ReadBig(this PointerSpan ps, ref int offset, out T value) where T : unmanaged, Enum - { - value = ps.ReadEnumBig(offset); - offset += Unsafe.SizeOf(); - } - - private readonly unsafe struct PointerSpan : IList, IReadOnlyList, ICollection - where T : unmanaged - { - public readonly T* Pointer; - - public PointerSpan(T* pointer, int count) - { - this.Pointer = pointer; - this.Count = count; - } - - public PointerSpan(nint pointer, int count) - : this((T*)pointer, count) - { - } - - public Span Span => new(this.Pointer, this.Count); - - public bool IsEmpty => this.Count == 0; - - public int Count { get; } - - public int Length => this.Count; - - public int ByteCount => sizeof(T) * this.Count; - - bool ICollection.IsSynchronized => false; - - object ICollection.SyncRoot => this; - - bool ICollection.IsReadOnly => false; - - public ref T this[int index] => ref this.Pointer[this.EnsureIndex(index)]; - - public PointerSpan this[Range range] => this.Slice(range.GetOffsetAndLength(this.Count)); - - T IList.this[int index] - { - get => this.Pointer[this.EnsureIndex(index)]; - set => this.Pointer[this.EnsureIndex(index)] = value; - } - - T IReadOnlyList.this[int index] => this.Pointer[this.EnsureIndex(index)]; - - public bool ContainsPointer(T2* obj) where T2 : unmanaged => - (T*)obj >= this.Pointer && (T*)(obj + 1) <= this.Pointer + this.Count; - - public PointerSpan Slice(int offset, int count) => new(this.Pointer + offset, count); - - public PointerSpan Slice((int Offset, int Count) offsetAndCount) - => this.Slice(offsetAndCount.Offset, offsetAndCount.Count); - - public PointerSpan As(int count) - where T2 : unmanaged => - count > this.Count / sizeof(T2) - ? throw new ArgumentOutOfRangeException( - nameof(count), - count, - $"Wanted {count} items; had {this.Count / sizeof(T2)} items") - : new((T2*)this.Pointer, count); - - public PointerSpan As() - where T2 : unmanaged => - new((T2*)this.Pointer, this.Count / sizeof(T2)); - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < this.Count; i++) - yield return this[i]; - } - - void ICollection.Add(T item) => throw new NotSupportedException(); - - void ICollection.Clear() => throw new NotSupportedException(); - - bool ICollection.Contains(T item) - { - for (var i = 0; i < this.Count; i++) - { - if (Equals(this.Pointer[i], item)) - return true; - } - - return false; - } - - void ICollection.CopyTo(T[] array, int arrayIndex) - { - if (array.Length < this.Count) - throw new ArgumentException(null, nameof(array)); - - if (array.Length < arrayIndex + this.Count) - throw new ArgumentException(null, nameof(arrayIndex)); - - for (var i = 0; i < this.Count; i++) - array[arrayIndex + i] = this.Pointer[i]; - } - - bool ICollection.Remove(T item) => throw new NotSupportedException(); - - int IList.IndexOf(T item) - { - for (var i = 0; i < this.Count; i++) - { - if (Equals(this.Pointer[i], item)) - return i; - } - - return -1; - } - - void IList.Insert(int index, T item) => throw new NotSupportedException(); - - void IList.RemoveAt(int index) => throw new NotSupportedException(); - - void ICollection.CopyTo(Array array, int arrayIndex) - { - if (array.Length < this.Count) - throw new ArgumentException(null, nameof(array)); - - if (array.Length < arrayIndex + this.Count) - throw new ArgumentException(null, nameof(arrayIndex)); - - for (var i = 0; i < this.Count; i++) - array.SetValue(this.Pointer[i], arrayIndex + i); - } - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - private int EnsureIndex(int index) => - index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); - } - - private readonly unsafe struct BigEndianPointerSpan - : IList, IReadOnlyList, ICollection - where T : unmanaged - { - public readonly T* Pointer; - - private readonly Func reverseEndianness; - - public BigEndianPointerSpan(PointerSpan pointerSpan, Func reverseEndianness) - { - this.reverseEndianness = reverseEndianness; - this.Pointer = pointerSpan.Pointer; - this.Count = pointerSpan.Count; - } - - public int Count { get; } - - public int Length => this.Count; - - public int ByteCount => sizeof(T) * this.Count; - - public bool IsSynchronized => true; - - public object SyncRoot => this; - - public bool IsReadOnly => true; - - public T this[int index] - { - get => - BitConverter.IsLittleEndian - ? this.reverseEndianness(this.Pointer[this.EnsureIndex(index)]) - : this.Pointer[this.EnsureIndex(index)]; - set => this.Pointer[this.EnsureIndex(index)] = - BitConverter.IsLittleEndian - ? this.reverseEndianness(value) - : value; - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < this.Count; i++) - yield return this[i]; - } - - void ICollection.Add(T item) => throw new NotSupportedException(); - - void ICollection.Clear() => throw new NotSupportedException(); - - bool ICollection.Contains(T item) => throw new NotSupportedException(); - - void ICollection.CopyTo(T[] array, int arrayIndex) - { - if (array.Length < this.Count) - throw new ArgumentException(null, nameof(array)); - - if (array.Length < arrayIndex + this.Count) - throw new ArgumentException(null, nameof(arrayIndex)); - - for (var i = 0; i < this.Count; i++) - array[arrayIndex + i] = this[i]; - } - - bool ICollection.Remove(T item) => throw new NotSupportedException(); - - int IList.IndexOf(T item) - { - for (var i = 0; i < this.Count; i++) - { - if (Equals(this[i], item)) - return i; - } - - return -1; - } - - void IList.Insert(int index, T item) => throw new NotSupportedException(); - - void IList.RemoveAt(int index) => throw new NotSupportedException(); - - void ICollection.CopyTo(Array array, int arrayIndex) - { - if (array.Length < this.Count) - throw new ArgumentException(null, nameof(array)); - - if (array.Length < arrayIndex + this.Count) - throw new ArgumentException(null, nameof(arrayIndex)); - - for (var i = 0; i < this.Count; i++) - array.SetValue(this[i], arrayIndex + i); - } - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - private int EnsureIndex(int index) => - index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs deleted file mode 100644 index 80cf4b7da..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs +++ /dev/null @@ -1,1391 +0,0 @@ -using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] -[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] -[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] -internal static partial class TrueTypeUtils -{ - [Flags] - private enum ValueFormat : ushort - { - PlacementX = 1 << 0, - PlacementY = 1 << 1, - AdvanceX = 1 << 2, - AdvanceY = 1 << 3, - PlacementDeviceOffsetX = 1 << 4, - PlacementDeviceOffsetY = 1 << 5, - AdvanceDeviceOffsetX = 1 << 6, - AdvanceDeviceOffsetY = 1 << 7, - - ValidBits = 0 - | PlacementX | PlacementY - | AdvanceX | AdvanceY - | PlacementDeviceOffsetX | PlacementDeviceOffsetY - | AdvanceDeviceOffsetX | AdvanceDeviceOffsetY, - } - - private static int NumBytes(this ValueFormat value) => - ushort.PopCount((ushort)(value & ValueFormat.ValidBits)) * 2; - - private readonly struct Cmap - { - // https://docs.microsoft.com/en-us/typography/opentype/spec/cmap - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html - - public static readonly TagStruct DirectoryTableTag = new('c', 'm', 'a', 'p'); - - public readonly PointerSpan Memory; - - public Cmap(SfntFile file) - : this(file[DirectoryTableTag]) - { - } - - public Cmap(PointerSpan memory) => this.Memory = memory; - - public ushort Version => this.Memory.ReadU16Big(0); - - public ushort RecordCount => this.Memory.ReadU16Big(2); - - public BigEndianPointerSpan Records => new( - this.Memory[4..].As(this.RecordCount), - EncodingRecord.ReverseEndianness); - - public EncodingRecord? UnicodeEncodingRecord => - this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( - x => x!.Value.PlatformAndEncoding is - { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Bmp }) - ?? - this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( - x => x!.Value.PlatformAndEncoding is - { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Full }) - ?? - this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( - x => x!.Value.PlatformAndEncoding is - { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.UnicodeFullRepertoire }) - ?? - this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( - x => x!.Value.PlatformAndEncoding is - { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeBmp }) - ?? - this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( - x => x!.Value.PlatformAndEncoding is - { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeFullRepertoire }); - - public CmapFormat? UnicodeTable => this.GetTable(this.UnicodeEncodingRecord); - - public CmapFormat? GetTable(EncodingRecord? encodingRecord) => - encodingRecord is { } record - ? this.Memory.ReadU16Big(record.SubtableOffset) switch - { - 0 => new CmapFormat0(this.Memory[record.SubtableOffset..]), - 2 => new CmapFormat2(this.Memory[record.SubtableOffset..]), - 4 => new CmapFormat4(this.Memory[record.SubtableOffset..]), - 6 => new CmapFormat6(this.Memory[record.SubtableOffset..]), - 8 => new CmapFormat8(this.Memory[record.SubtableOffset..]), - 10 => new CmapFormat10(this.Memory[record.SubtableOffset..]), - 12 or 13 => new CmapFormat12And13(this.Memory[record.SubtableOffset..]), - _ => null, - } - : null; - - public struct EncodingRecord - { - public PlatformAndEncoding PlatformAndEncoding; - public int SubtableOffset; - - public EncodingRecord(PointerSpan span) - { - this.PlatformAndEncoding = new(span); - var offset = Unsafe.SizeOf(); - span.ReadBig(ref offset, out this.SubtableOffset); - } - - public static EncodingRecord ReverseEndianness(EncodingRecord value) => new() - { - PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), - SubtableOffset = BinaryPrimitives.ReverseEndianness(value.SubtableOffset), - }; - } - - public struct MapGroup : IComparable - { - public int StartCharCode; - public int EndCharCode; - public int GlyphId; - - public MapGroup(PointerSpan span) - { - var offset = 0; - span.ReadBig(ref offset, out this.StartCharCode); - span.ReadBig(ref offset, out this.EndCharCode); - span.ReadBig(ref offset, out this.GlyphId); - } - - public static MapGroup ReverseEndianness(MapGroup obj) => new() - { - StartCharCode = BinaryPrimitives.ReverseEndianness(obj.StartCharCode), - EndCharCode = BinaryPrimitives.ReverseEndianness(obj.EndCharCode), - GlyphId = BinaryPrimitives.ReverseEndianness(obj.GlyphId), - }; - - public int CompareTo(MapGroup other) - { - var endCharCodeComparison = this.EndCharCode.CompareTo(other.EndCharCode); - if (endCharCodeComparison != 0) return endCharCodeComparison; - - var startCharCodeComparison = this.StartCharCode.CompareTo(other.StartCharCode); - if (startCharCodeComparison != 0) return startCharCodeComparison; - - return this.GlyphId.CompareTo(other.GlyphId); - } - } - - public abstract class CmapFormat : IReadOnlyDictionary - { - public int Count => this.Count(x => x.Value != 0); - - public IEnumerable Keys => this.Select(x => x.Key); - - public IEnumerable Values => this.Select(x => x.Value); - - public ushort this[int key] => throw new NotImplementedException(); - - public abstract ushort CharToGlyph(int c); - - public abstract IEnumerator> GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - public bool ContainsKey(int key) => this.CharToGlyph(key) != 0; - - public bool TryGetValue(int key, out ushort value) - { - value = this.CharToGlyph(key); - return value != 0; - } - } - - public class CmapFormat0 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat0(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort Length => this.Memory.ReadU16Big(2); - - public ushort Language => this.Memory.ReadU16Big(4); - - public PointerSpan GlyphIdArray => this.Memory.Slice(6, 256); - - public override ushort CharToGlyph(int c) => c is >= 0 and < 256 ? this.GlyphIdArray[c] : (byte)0; - - public override IEnumerator> GetEnumerator() - { - for (var codepoint = 0; codepoint < 256; codepoint++) - { - if (this.GlyphIdArray[codepoint] is var glyphId and not 0) - yield return new(codepoint, glyphId); - } - } - } - - public class CmapFormat2 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat2(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort Length => this.Memory.ReadU16Big(2); - - public ushort Language => this.Memory.ReadU16Big(4); - - public BigEndianPointerSpan SubHeaderKeys => new( - this.Memory[6..].As(256), - BinaryPrimitives.ReverseEndianness); - - public PointerSpan Data => this.Memory[518..]; - - public bool TryGetSubHeader( - int keyIndex, out SubHeader subheader, out BigEndianPointerSpan glyphSpan) - { - if (keyIndex < 0 || keyIndex >= this.SubHeaderKeys.Count) - { - subheader = default; - glyphSpan = default; - return false; - } - - var offset = this.SubHeaderKeys[keyIndex]; - if (offset + Unsafe.SizeOf() > this.Data.Length) - { - subheader = default; - glyphSpan = default; - return false; - } - - subheader = new(this.Data[offset..]); - glyphSpan = new( - this.Data[(offset + Unsafe.SizeOf() + subheader.IdRangeOffset)..] - .As(subheader.EntryCount), - BinaryPrimitives.ReverseEndianness); - - return true; - } - - public override ushort CharToGlyph(int c) - { - if (!this.TryGetSubHeader(c >> 8, out var sh, out var glyphSpan)) - return 0; - - c = (c & 0xFF) - sh.FirstCode; - if (c > 0 || c >= glyphSpan.Count) - return 0; - - var res = glyphSpan[c]; - return res == 0 ? (ushort)0 : unchecked((ushort)(res + sh.IdDelta)); - } - - public override IEnumerator> GetEnumerator() - { - for (var i = 0; i < this.SubHeaderKeys.Count; i++) - { - if (!this.TryGetSubHeader(i, out var sh, out var glyphSpan)) - continue; - - for (var j = 0; j < glyphSpan.Count; j++) - { - var res = glyphSpan[j]; - if (res == 0) - continue; - - var glyphId = unchecked((ushort)(res + sh.IdDelta)); - if (glyphId == 0) - continue; - - var codepoint = (i << 8) | (sh.FirstCode + j); - yield return new(codepoint, glyphId); - } - } - } - - public struct SubHeader - { - public ushort FirstCode; - public ushort EntryCount; - public ushort IdDelta; - public ushort IdRangeOffset; - - public SubHeader(PointerSpan span) - { - var offset = 0; - span.ReadBig(ref offset, out this.FirstCode); - span.ReadBig(ref offset, out this.EntryCount); - span.ReadBig(ref offset, out this.IdDelta); - span.ReadBig(ref offset, out this.IdRangeOffset); - } - } - } - - public class CmapFormat4 : CmapFormat - { - public const int EndCodesOffset = 14; - - public readonly PointerSpan Memory; - - public CmapFormat4(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort Length => this.Memory.ReadU16Big(2); - - public ushort Language => this.Memory.ReadU16Big(4); - - public ushort SegCountX2 => this.Memory.ReadU16Big(6); - - public ushort SearchRange => this.Memory.ReadU16Big(8); - - public ushort EntrySelector => this.Memory.ReadU16Big(10); - - public ushort RangeShift => this.Memory.ReadU16Big(12); - - public BigEndianPointerSpan EndCodes => new( - this.Memory.Slice(EndCodesOffset, this.SegCountX2).As(), - BinaryPrimitives.ReverseEndianness); - - public BigEndianPointerSpan StartCodes => new( - this.Memory.Slice(EndCodesOffset + 2 + (1 * this.SegCountX2), this.SegCountX2).As(), - BinaryPrimitives.ReverseEndianness); - - public BigEndianPointerSpan IdDeltas => new( - this.Memory.Slice(EndCodesOffset + 2 + (2 * this.SegCountX2), this.SegCountX2).As(), - BinaryPrimitives.ReverseEndianness); - - public BigEndianPointerSpan IdRangeOffsets => new( - this.Memory.Slice(EndCodesOffset + 2 + (3 * this.SegCountX2), this.SegCountX2).As(), - BinaryPrimitives.ReverseEndianness); - - public BigEndianPointerSpan GlyphIds => new( - this.Memory.Slice(EndCodesOffset + 2 + (4 * this.SegCountX2), this.SegCountX2).As(), - BinaryPrimitives.ReverseEndianness); - - public override ushort CharToGlyph(int c) - { - if (c is < 0 or >= 0x10000) - return 0; - - var i = this.EndCodes.BinarySearch((ushort)c); - if (i < 0) - return 0; - - var startCode = this.StartCodes[i]; - var endCode = this.EndCodes[i]; - if (c < startCode || c > endCode) - return 0; - - var idRangeOffset = this.IdRangeOffsets[i]; - var idDelta = this.IdDeltas[i]; - if (idRangeOffset == 0) - return unchecked((ushort)(c + idDelta)); - - var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; - if (ptr > this.Memory.Length) - return 0; - - var glyphs = new BigEndianPointerSpan( - this.Memory[ptr..].As(endCode - startCode + 1), - BinaryPrimitives.ReverseEndianness); - - var glyph = glyphs[c - startCode]; - return unchecked(glyph == 0 ? (ushort)0 : (ushort)(idDelta + glyph)); - } - - public override IEnumerator> GetEnumerator() - { - var startCodes = this.StartCodes; - var endCodes = this.EndCodes; - var idDeltas = this.IdDeltas; - var idRangeOffsets = this.IdRangeOffsets; - - for (var i = 0; i < this.SegCountX2 / 2; i++) - { - var startCode = startCodes[i]; - var endCode = endCodes[i]; - var idRangeOffset = idRangeOffsets[i]; - var idDelta = idDeltas[i]; - - if (idRangeOffset == 0) - { - for (var c = (int)startCode; c <= endCode; c++) - yield return new(c, (ushort)(c + idDelta)); - } - else - { - var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; - if (ptr >= this.Memory.Length) - continue; - - var glyphs = new BigEndianPointerSpan( - this.Memory[ptr..].As(endCode - startCode + 1), - BinaryPrimitives.ReverseEndianness); - - for (var j = 0; j < glyphs.Count; j++) - { - var glyphId = glyphs[j]; - if (glyphId == 0) - continue; - - glyphId += idDelta; - if (glyphId == 0) - continue; - - yield return new(startCode + j, glyphId); - } - } - } - } - } - - public class CmapFormat6 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat6(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort Length => this.Memory.ReadU16Big(2); - - public ushort Language => this.Memory.ReadU16Big(4); - - public ushort FirstCode => this.Memory.ReadU16Big(6); - - public ushort EntryCount => this.Memory.ReadU16Big(8); - - public BigEndianPointerSpan GlyphIds => new( - this.Memory[10..].As(this.EntryCount), - BinaryPrimitives.ReverseEndianness); - - public override ushort CharToGlyph(int c) - { - var glyphIds = this.GlyphIds; - if (c < this.FirstCode || c >= this.FirstCode + this.GlyphIds.Count) - return 0; - - return glyphIds[c - this.FirstCode]; - } - - public override IEnumerator> GetEnumerator() - { - var glyphIds = this.GlyphIds; - for (var i = 0; i < this.GlyphIds.Length; i++) - { - var g = glyphIds[i]; - if (g != 0) - yield return new(this.FirstCode + i, g); - } - } - } - - public class CmapFormat8 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat8(PointerSpan memory) => this.Memory = memory; - - public int Format => this.Memory.ReadI32Big(0); - - public int Length => this.Memory.ReadI32Big(4); - - public int Language => this.Memory.ReadI32Big(8); - - public PointerSpan Is32 => this.Memory.Slice(12, 8192); - - public int NumGroups => this.Memory.ReadI32Big(8204); - - public BigEndianPointerSpan Groups => - new(this.Memory[8208..].As(), MapGroup.ReverseEndianness); - - public override ushort CharToGlyph(int c) - { - var groups = this.Groups; - - var i = groups.BinarySearch((in MapGroup value) => c.CompareTo(value.EndCharCode)); - if (i < 0) - return 0; - - var group = groups[i]; - if (c < group.StartCharCode || c > group.EndCharCode) - return 0; - - return unchecked((ushort)(group.GlyphId + c - group.StartCharCode)); - } - - public override IEnumerator> GetEnumerator() - { - foreach (var group in this.Groups) - { - for (var j = group.StartCharCode; j <= group.EndCharCode; j++) - { - var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); - if (glyphId == 0) - continue; - - yield return new(j, glyphId); - } - } - } - } - - public class CmapFormat10 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat10(PointerSpan memory) => this.Memory = memory; - - public int Format => this.Memory.ReadI32Big(0); - - public int Length => this.Memory.ReadI32Big(4); - - public int Language => this.Memory.ReadI32Big(8); - - public int StartCharCode => this.Memory.ReadI32Big(12); - - public int NumChars => this.Memory.ReadI32Big(16); - - public BigEndianPointerSpan GlyphIdArray => new( - this.Memory.Slice(20, this.NumChars * 2).As(), - BinaryPrimitives.ReverseEndianness); - - public override ushort CharToGlyph(int c) - { - if (c < this.StartCharCode || c >= this.StartCharCode + this.GlyphIdArray.Count) - return 0; - - return this.GlyphIdArray[c]; - } - - public override IEnumerator> GetEnumerator() - { - for (var i = 0; i < this.GlyphIdArray.Count; i++) - { - var glyph = this.GlyphIdArray[i]; - if (glyph != 0) - yield return new(this.StartCharCode + i, glyph); - } - } - } - - public class CmapFormat12And13 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat12And13(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public int Length => this.Memory.ReadI32Big(4); - - public int Language => this.Memory.ReadI32Big(8); - - public int NumGroups => this.Memory.ReadI32Big(12); - - public BigEndianPointerSpan Groups => new( - this.Memory[16..].As(this.NumGroups), - MapGroup.ReverseEndianness); - - public override ushort CharToGlyph(int c) - { - var groups = this.Groups; - - var i = groups.BinarySearch(new MapGroup() { EndCharCode = c }); - if (i < 0) - return 0; - - var group = groups[i]; - if (c < group.StartCharCode || c > group.EndCharCode) - return 0; - - if (this.Format == 12) - return (ushort)(group.GlyphId + c - group.StartCharCode); - else - return (ushort)group.GlyphId; - } - - public override IEnumerator> GetEnumerator() - { - var groups = this.Groups; - if (this.Format == 12) - { - foreach (var group in groups) - { - for (var j = group.StartCharCode; j <= group.EndCharCode; j++) - { - var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); - if (glyphId == 0) - continue; - - yield return new(j, glyphId); - } - } - } - else - { - foreach (var group in groups) - { - if (group.GlyphId == 0) - continue; - - for (var j = group.StartCharCode; j <= group.EndCharCode; j++) - yield return new(j, (ushort)group.GlyphId); - } - } - } - } - } - - private readonly struct Gpos - { - // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos - - public static readonly TagStruct DirectoryTableTag = new('G', 'P', 'O', 'S'); - - public readonly PointerSpan Memory; - - public Gpos(SfntFile file) - : this(file[DirectoryTableTag]) - { - } - - public Gpos(PointerSpan memory) => this.Memory = memory; - - public Fixed Version => new(this.Memory); - - public ushort ScriptListOffset => this.Memory.ReadU16Big(4); - - public ushort FeatureListOffset => this.Memory.ReadU16Big(6); - - public ushort LookupListOffset => this.Memory.ReadU16Big(8); - - public uint FeatureVariationsOffset => this.Version.CompareTo(new(1, 1)) >= 0 - ? this.Memory.ReadU32Big(10) - : 0; - - public BigEndianPointerSpan LookupOffsetList => new( - this.Memory[(this.LookupListOffset + 2)..].As( - this.Memory.ReadU16Big(this.LookupListOffset)), - BinaryPrimitives.ReverseEndianness); - - public IEnumerable EnumerateLookupTables() - { - foreach (var offset in this.LookupOffsetList) - yield return new(this.Memory[(this.LookupListOffset + offset)..]); - } - - public IEnumerable ExtractAdvanceX() => - this.EnumerateLookupTables() - .SelectMany( - lookupTable => lookupTable.Type switch - { - LookupType.PairAdjustment => - lookupTable.SelectMany(y => new PairAdjustmentPositioning(y).ExtractAdvanceX()), - LookupType.ExtensionPositioning => - lookupTable - .Where(y => y.ReadU16Big(0) == 1) - .Select(y => new ExtensionPositioningSubtableFormat1(y)) - .Where(y => y.ExtensionLookupType == LookupType.PairAdjustment) - .SelectMany(y => new PairAdjustmentPositioning(y.ExtensionData).ExtractAdvanceX()), - _ => Array.Empty(), - }); - - public struct ValueRecord - { - public short PlacementX; - public short PlacementY; - public short AdvanceX; - public short AdvanceY; - public short PlacementDeviceOffsetX; - public short PlacementDeviceOffsetY; - public short AdvanceDeviceOffsetX; - public short AdvanceDeviceOffsetY; - - public ValueRecord(PointerSpan pointerSpan, ValueFormat valueFormat) - { - var offset = 0; - if ((valueFormat & ValueFormat.PlacementX) != 0) - pointerSpan.ReadBig(ref offset, out this.PlacementX); - - if ((valueFormat & ValueFormat.PlacementY) != 0) - pointerSpan.ReadBig(ref offset, out this.PlacementY); - - if ((valueFormat & ValueFormat.AdvanceX) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceX); - if ((valueFormat & ValueFormat.AdvanceY) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceY); - if ((valueFormat & ValueFormat.PlacementDeviceOffsetX) != 0) - pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetX); - - if ((valueFormat & ValueFormat.PlacementDeviceOffsetY) != 0) - pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetY); - - if ((valueFormat & ValueFormat.AdvanceDeviceOffsetX) != 0) - pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetX); - - if ((valueFormat & ValueFormat.AdvanceDeviceOffsetY) != 0) - pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetY); - } - } - - public readonly struct PairAdjustmentPositioning - { - public readonly PointerSpan Memory; - - public PairAdjustmentPositioning(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public IEnumerable ExtractAdvanceX() => this.Format switch - { - 1 => new Format1(this.Memory).ExtractAdvanceX(), - 2 => new Format2(this.Memory).ExtractAdvanceX(), - _ => Array.Empty(), - }; - - public readonly struct Format1 - { - public readonly PointerSpan Memory; - - public Format1(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort CoverageOffset => this.Memory.ReadU16Big(2); - - public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); - - public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); - - public ushort PairSetCount => this.Memory.ReadU16Big(8); - - public BigEndianPointerSpan PairSetOffsets => new( - this.Memory[10..].As(this.PairSetCount), - BinaryPrimitives.ReverseEndianness); - - public CoverageTable CoverageTable => new(this.Memory[this.CoverageOffset..]); - - public PairSet this[int index] => new( - this.Memory[this.PairSetOffsets[index] ..], - this.ValueFormat1, - this.ValueFormat2); - - public IEnumerable ExtractAdvanceX() - { - if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && - (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) - { - yield break; - } - - var coverageTable = this.CoverageTable; - switch (coverageTable.Format) - { - case CoverageTable.CoverageFormat.Glyphs: - { - var glyphSpan = coverageTable.Glyphs; - foreach (var coverageIndex in Enumerable.Range(0, glyphSpan.Count)) - { - var glyph1Id = glyphSpan[coverageIndex]; - PairSet pairSetView; - try - { - pairSetView = this[coverageIndex]; - } - catch (ArgumentOutOfRangeException) - { - yield break; - } - catch (IndexOutOfRangeException) - { - yield break; - } - - foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) - { - var pair = pairSetView[pairIndex]; - var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); - if (adj >= 10000) - System.Diagnostics.Debugger.Break(); - - if (adj != 0) - yield return new(glyph1Id, pair.SecondGlyph, adj); - } - } - - break; - } - - case CoverageTable.CoverageFormat.RangeRecords: - { - foreach (var rangeRecord in coverageTable.RangeRecords) - { - var startGlyphId = rangeRecord.StartGlyphId; - var endGlyphId = rangeRecord.EndGlyphId; - var startCoverageIndex = rangeRecord.StartCoverageIndex; - var glyphCount = endGlyphId - startGlyphId + 1; - foreach (var glyph1Id in Enumerable.Range(startGlyphId, glyphCount)) - { - PairSet pairSetView; - try - { - pairSetView = this[startCoverageIndex + glyph1Id - startGlyphId]; - } - catch (ArgumentOutOfRangeException) - { - yield break; - } - catch (IndexOutOfRangeException) - { - yield break; - } - - foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) - { - var pair = pairSetView[pairIndex]; - var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); - if (adj != 0) - yield return new((ushort)glyph1Id, pair.SecondGlyph, adj); - } - } - } - - break; - } - } - } - - public readonly struct PairSet - { - public readonly PointerSpan Memory; - public readonly ValueFormat ValueFormat1; - public readonly ValueFormat ValueFormat2; - public readonly int PairValue1Size; - public readonly int PairValue2Size; - public readonly int PairSize; - - public PairSet( - PointerSpan memory, - ValueFormat valueFormat1, - ValueFormat valueFormat2) - { - this.Memory = memory; - this.ValueFormat1 = valueFormat1; - this.ValueFormat2 = valueFormat2; - this.PairValue1Size = this.ValueFormat1.NumBytes(); - this.PairValue2Size = this.ValueFormat2.NumBytes(); - this.PairSize = 2 + this.PairValue1Size + this.PairValue2Size; - } - - public ushort Count => this.Memory.ReadU16Big(0); - - public PairValueRecord this[int index] - { - get - { - var pvr = this.Memory.Slice(2 + (this.PairSize * index), this.PairSize); - return new() - { - SecondGlyph = pvr.ReadU16Big(0), - Record1 = new(pvr.Slice(2, this.PairValue1Size), this.ValueFormat1), - Record2 = new( - pvr.Slice(2 + this.PairValue1Size, this.PairValue2Size), - this.ValueFormat2), - }; - } - } - - public struct PairValueRecord - { - public ushort SecondGlyph; - public ValueRecord Record1; - public ValueRecord Record2; - } - } - } - - public readonly struct Format2 - { - public readonly PointerSpan Memory; - public readonly int PairValue1Size; - public readonly int PairValue2Size; - public readonly int PairSize; - - public Format2(PointerSpan memory) - { - this.Memory = memory; - this.PairValue1Size = this.ValueFormat1.NumBytes(); - this.PairValue2Size = this.ValueFormat2.NumBytes(); - this.PairSize = this.PairValue1Size + this.PairValue2Size; - } - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort CoverageOffset => this.Memory.ReadU16Big(2); - - public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); - - public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); - - public ushort ClassDef1Offset => this.Memory.ReadU16Big(8); - - public ushort ClassDef2Offset => this.Memory.ReadU16Big(10); - - public ushort Class1Count => this.Memory.ReadU16Big(12); - - public ushort Class2Count => this.Memory.ReadU16Big(14); - - public ClassDefTable ClassDefTable1 => new(this.Memory[this.ClassDef1Offset..]); - - public ClassDefTable ClassDefTable2 => new(this.Memory[this.ClassDef2Offset..]); - - public (ValueRecord Record1, ValueRecord Record2) this[(int Class1Index, int Class2Index) v] => - this[v.Class1Index, v.Class2Index]; - - public (ValueRecord Record1, ValueRecord Record2) this[int class1Index, int class2Index] - { - get - { - if (class1Index < 0 || class1Index >= this.Class1Count) - throw new IndexOutOfRangeException(); - - if (class2Index < 0 || class2Index >= this.Class2Count) - throw new IndexOutOfRangeException(); - - var offset = 16 + (this.PairSize * ((class1Index * this.Class2Count) + class2Index)); - return ( - new(this.Memory.Slice(offset, this.PairValue1Size), this.ValueFormat1), - new( - this.Memory.Slice(offset + this.PairValue1Size, this.PairValue2Size), - this.ValueFormat2)); - } - } - - public IEnumerable ExtractAdvanceX() - { - if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && - (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) - { - yield break; - } - - var classes1 = this.ClassDefTable1.Enumerate() - .GroupBy(x => x.Class, x => x.GlyphId) - .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); - - var classes2 = this.ClassDefTable2.Enumerate() - .GroupBy(x => x.Class, x => x.GlyphId) - .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); - - foreach (var class1 in Enumerable.Range(0, this.Class1Count)) - { - if (!classes1.TryGetValue((ushort)class1, out var glyphs1)) - continue; - - foreach (var class2 in Enumerable.Range(0, this.Class2Count)) - { - if (!classes2.TryGetValue((ushort)class2, out var glyphs2)) - continue; - - (ValueRecord, ValueRecord) record; - try - { - record = this[class1, class2]; - } - catch (ArgumentOutOfRangeException) - { - yield break; - } - catch (IndexOutOfRangeException) - { - yield break; - } - - var val = record.Item1.AdvanceX + record.Item2.PlacementX; - if (val == 0) - continue; - - foreach (var glyph1 in glyphs1) - { - foreach (var glyph2 in glyphs2) - { - yield return new(glyph1, glyph2, (short)val); - } - } - } - } - } - } - } - - public readonly struct ExtensionPositioningSubtableFormat1 - { - public readonly PointerSpan Memory; - - public ExtensionPositioningSubtableFormat1(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public LookupType ExtensionLookupType => this.Memory.ReadEnumBig(2); - - public int ExtensionOffset => this.Memory.ReadI32Big(4); - - public PointerSpan ExtensionData => this.Memory[this.ExtensionOffset..]; - } - } - - private readonly struct Head - { - // https://docs.microsoft.com/en-us/typography/opentype/spec/head - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html - - public const uint MagicNumberValue = 0x5F0F3CF5; - public static readonly TagStruct DirectoryTableTag = new('h', 'e', 'a', 'd'); - - public readonly PointerSpan Memory; - - public Head(SfntFile file) - : this(file[DirectoryTableTag]) - { - } - - public Head(PointerSpan memory) => this.Memory = memory; - - [Flags] - public enum HeadFlags : ushort - { - BaselineForFontAtZeroY = 1 << 0, - LeftSideBearingAtZeroX = 1 << 1, - InstructionsDependOnPointSize = 1 << 2, - ForcePpemsInteger = 1 << 3, - InstructionsAlterAdvanceWidth = 1 << 4, - VerticalLayout = 1 << 5, - Reserved6 = 1 << 6, - RequiresLayoutForCorrectLinguisticRendering = 1 << 7, - IsAatFont = 1 << 8, - ContainsRtlGlyph = 1 << 9, - ContainsIndicStyleRearrangementEffects = 1 << 10, - Lossless = 1 << 11, - ProduceCompatibleMetrics = 1 << 12, - OptimizedForClearType = 1 << 13, - IsLastResortFont = 1 << 14, - Reserved15 = 1 << 15, - } - - [Flags] - public enum MacStyleFlags : ushort - { - Bold = 1 << 0, - Italic = 1 << 1, - Underline = 1 << 2, - Outline = 1 << 3, - Shadow = 1 << 4, - Condensed = 1 << 5, - Extended = 1 << 6, - } - - public Fixed Version => new(this.Memory); - - public Fixed FontRevision => new(this.Memory[4..]); - - public uint ChecksumAdjustment => this.Memory.ReadU32Big(8); - - public uint MagicNumber => this.Memory.ReadU32Big(12); - - public HeadFlags Flags => this.Memory.ReadEnumBig(16); - - public ushort UnitsPerEm => this.Memory.ReadU16Big(18); - - public ulong CreatedTimestamp => this.Memory.ReadU64Big(20); - - public ulong ModifiedTimestamp => this.Memory.ReadU64Big(28); - - public ushort MinX => this.Memory.ReadU16Big(36); - - public ushort MinY => this.Memory.ReadU16Big(38); - - public ushort MaxX => this.Memory.ReadU16Big(40); - - public ushort MaxY => this.Memory.ReadU16Big(42); - - public MacStyleFlags MacStyle => this.Memory.ReadEnumBig(44); - - public ushort LowestRecommendedPpem => this.Memory.ReadU16Big(46); - - public ushort FontDirectionHint => this.Memory.ReadU16Big(48); - - public ushort IndexToLocFormat => this.Memory.ReadU16Big(50); - - public ushort GlyphDataFormat => this.Memory.ReadU16Big(52); - } - - private readonly struct Kern - { - // https://docs.microsoft.com/en-us/typography/opentype/spec/kern - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html - - public static readonly TagStruct DirectoryTableTag = new('k', 'e', 'r', 'n'); - - public readonly PointerSpan Memory; - - public Kern(SfntFile file) - : this(file[DirectoryTableTag]) - { - } - - public Kern(PointerSpan memory) => this.Memory = memory; - - public ushort Version => this.Memory.ReadU16Big(0); - - public IEnumerable EnumerateHorizontalPairs() => this.Version switch - { - 0 => new Version0(this.Memory).EnumerateHorizontalPairs(), - 1 => new Version1(this.Memory).EnumerateHorizontalPairs(), - _ => Array.Empty(), - }; - - public readonly struct Format0 - { - public readonly PointerSpan Memory; - - public Format0(PointerSpan memory) => this.Memory = memory; - - public ushort PairCount => this.Memory.ReadU16Big(0); - - public ushort SearchRange => this.Memory.ReadU16Big(2); - - public ushort EntrySelector => this.Memory.ReadU16Big(4); - - public ushort RangeShift => this.Memory.ReadU16Big(6); - - public BigEndianPointerSpan Pairs => new( - this.Memory[8..].As(this.PairCount), - KerningPair.ReverseEndianness); - } - - public readonly struct Version0 - { - public readonly PointerSpan Memory; - - public Version0(PointerSpan memory) => this.Memory = memory; - - [Flags] - public enum CoverageFlags : byte - { - Horizontal = 1 << 0, - Minimum = 1 << 1, - CrossStream = 1 << 2, - Override = 1 << 3, - } - - public ushort Version => this.Memory.ReadU16Big(0); - - public ushort NumSubtables => this.Memory.ReadU16Big(2); - - public PointerSpan Data => this.Memory[4..]; - - public IEnumerable EnumerateSubtables() - { - var data = this.Data; - for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) - { - var st = new Subtable(data); - data = data[st.Length..]; - yield return st; - } - } - - public IEnumerable EnumerateHorizontalPairs() - { - var accumulator = new Dictionary<(ushort Left, ushort Right), short>(); - foreach (var subtable in this.EnumerateSubtables()) - { - var isOverride = (subtable.Flags & CoverageFlags.Override) != 0; - var isMinimum = (subtable.Flags & CoverageFlags.Minimum) != 0; - foreach (var t in subtable.EnumeratePairs()) - { - if (isOverride) - { - accumulator[(t.Left, t.Right)] = t.Value; - } - else if (isMinimum) - { - accumulator[(t.Left, t.Right)] = Math.Max( - accumulator.GetValueOrDefault((t.Left, t.Right), t.Value), - t.Value); - } - else - { - accumulator[(t.Left, t.Right)] = (short)( - accumulator.GetValueOrDefault( - (t.Left, t.Right)) + t.Value); - } - } - } - - return accumulator.Select( - x => new KerningPair { Left = x.Key.Left, Right = x.Key.Right, Value = x.Value }); - } - - public readonly struct Subtable - { - public readonly PointerSpan Memory; - - public Subtable(PointerSpan memory) => this.Memory = memory; - - public ushort Version => this.Memory.ReadU16Big(0); - - public ushort Length => this.Memory.ReadU16Big(2); - - public byte Format => this.Memory[4]; - - public CoverageFlags Flags => this.Memory.ReadEnumBig(5); - - public PointerSpan Data => this.Memory[6..]; - - public IEnumerable EnumeratePairs() => this.Format switch - { - 0 => new Format0(this.Data).Pairs, - _ => Array.Empty(), - }; - } - } - - public readonly struct Version1 - { - public readonly PointerSpan Memory; - - public Version1(PointerSpan memory) => this.Memory = memory; - - [Flags] - public enum CoverageFlags : byte - { - Vertical = 1 << 0, - CrossStream = 1 << 1, - Variation = 1 << 2, - } - - public Fixed Version => new(this.Memory); - - public int NumSubtables => this.Memory.ReadI16Big(4); - - public PointerSpan Data => this.Memory[8..]; - - public IEnumerable EnumerateSubtables() - { - var data = this.Data; - for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) - { - var st = new Subtable(data); - data = data[st.Length..]; - yield return st; - } - } - - public IEnumerable EnumerateHorizontalPairs() => this - .EnumerateSubtables() - .Where(x => x.Flags == 0) - .SelectMany(x => x.EnumeratePairs()); - - public readonly struct Subtable - { - public readonly PointerSpan Memory; - - public Subtable(PointerSpan memory) => this.Memory = memory; - - public int Length => this.Memory.ReadI32Big(0); - - public byte Format => this.Memory[4]; - - public CoverageFlags Flags => this.Memory.ReadEnumBig(5); - - public ushort TupleIndex => this.Memory.ReadU16Big(6); - - public PointerSpan Data => this.Memory[8..]; - - public IEnumerable EnumeratePairs() => this.Format switch - { - 0 => new Format0(this.Data).Pairs, - _ => Array.Empty(), - }; - } - } - } - - private readonly struct Name - { - // https://docs.microsoft.com/en-us/typography/opentype/spec/name - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html - - public static readonly TagStruct DirectoryTableTag = new('n', 'a', 'm', 'e'); - - public readonly PointerSpan Memory; - - public Name(SfntFile file) - : this(file[DirectoryTableTag]) - { - } - - public Name(PointerSpan memory) => this.Memory = memory; - - public ushort Version => this.Memory.ReadU16Big(0); - - public ushort Count => this.Memory.ReadU16Big(2); - - public ushort StorageOffset => this.Memory.ReadU16Big(4); - - public BigEndianPointerSpan NameRecords => new( - this.Memory[6..].As(this.Count), - NameRecord.ReverseEndianness); - - public ushort LanguageCount => - this.Version == 0 ? (ushort)0 : this.Memory.ReadU16Big(6 + this.NameRecords.ByteCount); - - public BigEndianPointerSpan LanguageRecords => this.Version == 0 - ? default - : new( - this.Memory[ - (8 + this.NameRecords - .ByteCount)..] - .As( - this.LanguageCount), - LanguageRecord.ReverseEndianness); - - public PointerSpan Storage => this.Memory[this.StorageOffset..]; - - public string this[in NameRecord record] => - record.PlatformAndEncoding.Decode(this.Storage.Span.Slice(record.StringOffset, record.Length)); - - public string this[in LanguageRecord record] => - Encoding.ASCII.GetString(this.Storage.Span.Slice(record.LanguageTagOffset, record.Length)); - - public struct NameRecord - { - public PlatformAndEncoding PlatformAndEncoding; - public ushort LanguageId; - public NameId NameId; - public ushort Length; - public ushort StringOffset; - - public NameRecord(PointerSpan span) - { - this.PlatformAndEncoding = new(span); - var offset = Unsafe.SizeOf(); - span.ReadBig(ref offset, out this.LanguageId); - span.ReadBig(ref offset, out this.NameId); - span.ReadBig(ref offset, out this.Length); - span.ReadBig(ref offset, out this.StringOffset); - } - - public static NameRecord ReverseEndianness(NameRecord value) => new() - { - PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), - LanguageId = BinaryPrimitives.ReverseEndianness(value.LanguageId), - NameId = (NameId)BinaryPrimitives.ReverseEndianness((ushort)value.NameId), - Length = BinaryPrimitives.ReverseEndianness(value.Length), - StringOffset = BinaryPrimitives.ReverseEndianness(value.StringOffset), - }; - } - - public struct LanguageRecord - { - public ushort Length; - public ushort LanguageTagOffset; - - public LanguageRecord(PointerSpan span) - { - var offset = 0; - span.ReadBig(ref offset, out this.Length); - span.ReadBig(ref offset, out this.LanguageTagOffset); - } - - public static LanguageRecord ReverseEndianness(LanguageRecord value) => new() - { - Length = BinaryPrimitives.ReverseEndianness(value.Length), - LanguageTagOffset = BinaryPrimitives.ReverseEndianness(value.LanguageTagOffset), - }; - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs deleted file mode 100644 index 1d437d56d..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Buffers.Binary; -using System.Collections.Generic; -using System.Linq; - -using Dalamud.Interface.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -internal static partial class TrueTypeUtils -{ - /// - /// Checks whether the given will fail in , - /// and throws an appropriate exception if it is the case. - /// - /// The font config. - public static unsafe void CheckImGuiCompatibleOrThrow(in ImFontConfig fontConfig) - { - var ranges = fontConfig.GlyphRanges; - var sfnt = AsSfntFile(fontConfig); - var cmap = new Cmap(sfnt); - if (cmap.UnicodeTable is not { } unicodeTable) - throw new NotSupportedException("The font does not have a compatible Unicode character mapping table."); - if (unicodeTable.All(x => !ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(x.Key, ranges))) - throw new NotSupportedException("The font does not have any glyph that falls under the requested range."); - } - - /// - /// Enumerates through horizontal pair adjustments of a kern and gpos tables. - /// - /// The font config. - /// The enumerable of pair adjustments. Distance values need to be multiplied by font size in pixels. - public static IEnumerable<(char Left, char Right, float Distance)> ExtractHorizontalPairAdjustments( - ImFontConfig fontConfig) - { - float multiplier; - Dictionary glyphToCodepoints; - Gpos gpos = default; - Kern kern = default; - - try - { - var sfnt = AsSfntFile(fontConfig); - var head = new Head(sfnt); - multiplier = 3f / 4 / head.UnitsPerEm; - - if (new Cmap(sfnt).UnicodeTable is not { } table) - yield break; - - if (sfnt.ContainsKey(Kern.DirectoryTableTag)) - kern = new(sfnt); - else if (sfnt.ContainsKey(Gpos.DirectoryTableTag)) - gpos = new(sfnt); - else - yield break; - - glyphToCodepoints = table - .GroupBy(x => x.Value, x => x.Key) - .OrderBy(x => x.Key) - .ToDictionary( - x => x.Key, - x => x.Where(y => y <= ushort.MaxValue) - .Select(y => (char)y) - .ToArray()); - } - catch - { - // don't care; give up - yield break; - } - - if (kern.Memory.Count != 0) - { - foreach (var pair in kern.EnumerateHorizontalPairs()) - { - if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) - continue; - if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) - continue; - - foreach (var l in leftChars) - { - foreach (var r in rightChars) - yield return (l, r, pair.Value * multiplier); - } - } - } - else if (gpos.Memory.Count != 0) - { - foreach (var pair in gpos.ExtractAdvanceX()) - { - if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) - continue; - if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) - continue; - - foreach (var l in leftChars) - { - foreach (var r in rightChars) - yield return (l, r, pair.Value * multiplier); - } - } - } - } - - private static unsafe SfntFile AsSfntFile(in ImFontConfig fontConfig) - { - var memory = new PointerSpan((byte*)fontConfig.FontData, fontConfig.FontDataSize); - if (memory.Length < 4) - throw new NotSupportedException("File is too short to even have a magic."); - - var magic = memory.ReadU32Big(0); - if (BitConverter.IsLittleEndian) - magic = BinaryPrimitives.ReverseEndianness(magic); - - if (magic == SfntFile.FileTagTrueType1.NativeValue) - return new(memory); - if (magic == SfntFile.FileTagType1.NativeValue) - return new(memory); - if (magic == SfntFile.FileTagOpenTypeWithCff.NativeValue) - return new(memory); - if (magic == SfntFile.FileTagOpenType1_0.NativeValue) - return new(memory); - if (magic == SfntFile.FileTagTrueTypeApple.NativeValue) - return new(memory); - if (magic == TtcFile.FileTag.NativeValue) - return new TtcFile(memory)[fontConfig.FontNo]; - - throw new NotSupportedException($"The given file with the magic 0x{magic:X08} is not supported."); - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs deleted file mode 100644 index cb7f7c65a..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs +++ /dev/null @@ -1,306 +0,0 @@ -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Text; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Managed version of , to avoid unnecessary heap allocation and use of unsafe blocks. -/// -public struct SafeFontConfig -{ - /// - /// The raw config. - /// - public ImFontConfig Raw; - - /// - /// Initializes a new instance of the struct. - /// - public SafeFontConfig() - { - this.OversampleH = 1; - this.OversampleV = 1; - this.PixelSnapH = true; - this.GlyphMaxAdvanceX = float.MaxValue; - this.RasterizerMultiply = 1f; - this.RasterizerGamma = 1.4f; - this.EllipsisChar = unchecked((char)-1); - this.Raw.FontDataOwnedByAtlas = 1; - } - - /// - /// Initializes a new instance of the struct, - /// copying applicable values from an existing instance of . - /// - /// Config to copy from. - public unsafe SafeFontConfig(ImFontConfigPtr config) - : this() - { - if (config.NativePtr is not null) - { - this.Raw = *config.NativePtr; - this.Raw.GlyphRanges = null; - } - } - - /// - /// Gets or sets the index of font within a TTF/OTF file. - /// - public int FontNo - { - get => this.Raw.FontNo; - set => this.Raw.FontNo = EnsureRange(value, 0, int.MaxValue); - } - - /// - /// Gets or sets the desired size of the new font, in pixels.
- /// Effectively, this is the line height.
- /// Value is tied with . - ///
- public float SizePx - { - get => this.Raw.SizePixels; - set => this.Raw.SizePixels = EnsureRange(value, float.Epsilon, float.MaxValue); - } - - /// - /// Gets or sets the desired size of the new font, in points.
- /// Effectively, this is the line height.
- /// Value is tied with . - ///
- public float SizePt - { - get => (this.Raw.SizePixels * 3) / 4; - set => this.Raw.SizePixels = EnsureRange((value * 4) / 3, float.Epsilon, float.MaxValue); - } - - /// - /// Gets or sets the horizontal oversampling pixel count.
- /// Rasterize at higher quality for sub-pixel positioning.
- /// Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory.
- /// Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. - ///
- public int OversampleH - { - get => this.Raw.OversampleH; - set => this.Raw.OversampleH = EnsureRange(value, 1, int.MaxValue); - } - - /// - /// Gets or sets the vertical oversampling pixel count.
- /// Rasterize at higher quality for sub-pixel positioning.
- /// This is not really useful as we don't use sub-pixel positions on the Y axis. - ///
- public int OversampleV - { - get => this.Raw.OversampleV; - set => this.Raw.OversampleV = EnsureRange(value, 1, int.MaxValue); - } - - /// - /// Gets or sets a value indicating whether to align every glyph to pixel boundary.
- /// Useful e.g. if you are merging a non-pixel aligned font with the default font.
- /// If enabled, you can set and to 1. - ///
- public bool PixelSnapH - { - get => this.Raw.PixelSnapH != 0; - set => this.Raw.PixelSnapH = value ? (byte)1 : (byte)0; - } - - /// - /// Gets or sets the extra spacing (in pixels) between glyphs.
- /// Only X axis is supported for now.
- /// Effectively, it is the letter spacing. - ///
- public Vector2 GlyphExtraSpacing - { - get => this.Raw.GlyphExtraSpacing; - set => this.Raw.GlyphExtraSpacing = new( - EnsureRange(value.X, float.MinValue, float.MaxValue), - EnsureRange(value.Y, float.MinValue, float.MaxValue)); - } - - /// - /// Gets or sets the offset all glyphs from this font input.
- /// Use this to offset fonts vertically when merging multiple fonts. - ///
- public Vector2 GlyphOffset - { - get => this.Raw.GlyphOffset; - set => this.Raw.GlyphOffset = new( - EnsureRange(value.X, float.MinValue, float.MaxValue), - EnsureRange(value.Y, float.MinValue, float.MaxValue)); - } - - /// - /// Gets or sets the glyph ranges, which is a user-provided list of Unicode range. - /// Each range has 2 values, and values are inclusive.
- /// The list must be zero-terminated.
- /// If empty or null, then all the glyphs from the font that is in the range of UCS-2 will be added. - ///
- public ushort[]? GlyphRanges { get; set; } - - /// - /// Gets or sets the minimum AdvanceX for glyphs.
- /// Set only to align font icons.
- /// Set both / to enforce mono-space font. - ///
- public float GlyphMinAdvanceX - { - get => this.Raw.GlyphMinAdvanceX; - set => this.Raw.GlyphMinAdvanceX = - float.IsFinite(value) - ? value - : throw new ArgumentOutOfRangeException( - nameof(value), - value, - $"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); - } - - /// - /// Gets or sets the maximum AdvanceX for glyphs. - /// - public float GlyphMaxAdvanceX - { - get => this.Raw.GlyphMaxAdvanceX; - set => this.Raw.GlyphMaxAdvanceX = - float.IsFinite(value) - ? value - : throw new ArgumentOutOfRangeException( - nameof(value), - value, - $"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); - } - - /// - /// Gets or sets a value that either brightens (>1.0f) or darkens (<1.0f) the font output.
- /// Brightening small fonts may be a good workaround to make them more readable. - ///
- public float RasterizerMultiply - { - get => this.Raw.RasterizerMultiply; - set => this.Raw.RasterizerMultiply = EnsureRange(value, float.Epsilon, float.MaxValue); - } - - /// - /// Gets or sets the gamma value for fonts. - /// - public float RasterizerGamma - { - get => this.Raw.RasterizerGamma; - set => this.Raw.RasterizerGamma = EnsureRange(value, float.Epsilon, float.MaxValue); - } - - /// - /// Gets or sets a value explicitly specifying unicode codepoint of the ellipsis character.
- /// When fonts are being merged first specified ellipsis will be used. - ///
- public char EllipsisChar - { - get => (char)this.Raw.EllipsisChar; - set => this.Raw.EllipsisChar = value; - } - - /// - /// Gets or sets the desired name of the new font. Names longer than 40 bytes will be partially lost. - /// - public unsafe string Name - { - get - { - fixed (void* pName = this.Raw.Name) - { - var span = new ReadOnlySpan(pName, 40); - var firstNull = span.IndexOf((byte)0); - if (firstNull != -1) - span = span[..firstNull]; - return Encoding.UTF8.GetString(span); - } - } - - set - { - fixed (void* pName = this.Raw.Name) - { - var span = new Span(pName, 40); - Encoding.UTF8.GetBytes(value, span); - } - } - } - - /// - /// Gets or sets the desired font to merge with, if set. - /// - public unsafe ImFontPtr MergeFont - { - get => this.Raw.DstFont is not null ? this.Raw.DstFont : default; - set - { - this.Raw.MergeMode = value.NativePtr is null ? (byte)0 : (byte)1; - this.Raw.DstFont = value.NativePtr is null ? default : value.NativePtr; - } - } - - /// - /// Throws with appropriate messages, - /// if this has invalid values. - /// - public readonly void ThrowOnInvalidValues() - { - if (!(this.Raw.FontNo >= 0)) - throw new ArgumentException($"{nameof(this.FontNo)} must not be a negative number."); - - if (!(this.Raw.SizePixels > 0)) - throw new ArgumentException($"{nameof(this.SizePx)} must be a positive number."); - - if (!(this.Raw.OversampleH >= 1)) - throw new ArgumentException($"{nameof(this.OversampleH)} must be a negative number."); - - if (!(this.Raw.OversampleV >= 1)) - throw new ArgumentException($"{nameof(this.OversampleV)} must be a negative number."); - - if (!float.IsFinite(this.Raw.GlyphMinAdvanceX)) - throw new ArgumentException($"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); - - if (!float.IsFinite(this.Raw.GlyphMaxAdvanceX)) - throw new ArgumentException($"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); - - if (!(this.Raw.RasterizerMultiply > 0)) - throw new ArgumentException($"{nameof(this.RasterizerMultiply)} must be a positive number."); - - if (!(this.Raw.RasterizerGamma > 0)) - throw new ArgumentException($"{nameof(this.RasterizerGamma)} must be a positive number."); - - if (this.GlyphRanges is { Length: > 0 } ranges) - { - if (ranges[0] == 0) - { - throw new ArgumentException( - "Font ranges cannot start with 0.", - nameof(this.GlyphRanges)); - } - - if (ranges[(ranges.Length - 1) & ~1] != 0) - { - throw new ArgumentException( - "Font ranges must terminate with a zero at even indices.", - nameof(this.GlyphRanges)); - } - } - } - - private static T EnsureRange(T value, T min, T max, [CallerMemberName] string callerName = "") - where T : INumber - { - if (value < min) - throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be less than {min}."); - if (value > max) - throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be more than {max}."); - - return value; - } -} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index a477ec09e..dd2e5bad3 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -11,8 +12,6 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -31,13 +30,11 @@ public sealed class UiBuilder : IDisposable private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); - private readonly Framework framework = Service.Get(); + private readonly GameFontManager gameFontManager = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -48,32 +45,14 @@ public sealed class UiBuilder : IDisposable /// The plugin namespace. internal UiBuilder(string namespaceName) { - try - { - this.stopwatch = new Stopwatch(); - this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); - this.namespaceName = namespaceName; + this.stopwatch = new Stopwatch(); + this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); + this.namespaceName = namespaceName; - this.interfaceManager.Draw += this.OnDraw; - this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw); - - this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; - this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); - - this.FontAtlas = - this.scopedFinalizer - .Add( - Service - .Get() - .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); - this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; - this.FontAtlas.RebuildRecommend += this.RebuildFonts; - } - catch - { - this.scopedFinalizer.Dispose(); - throw; - } + this.interfaceManager.Draw += this.OnDraw; + this.interfaceManager.BuildFonts += this.OnBuildFonts; + this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; + this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; } /// @@ -101,19 +80,19 @@ public sealed class UiBuilder : IDisposable /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler. + /// pointers inside this handler.
+ /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! ///
- [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] - public event Action? BuildFonts; + public event Action BuildFonts; /// /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler. + /// pointers inside this handler.
+ /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! ///
- [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] - public event Action? AfterBuildFonts; + public event Action AfterBuildFonts; /// /// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown. @@ -128,57 +107,18 @@ public sealed class UiBuilder : IDisposable public event Action HideUi; /// - /// Gets the default Dalamud font size in points. + /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. /// - public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt; - - /// - /// Gets the default Dalamud font size in pixels. - /// - public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx; - - /// - /// Gets the default Dalamud font - supporting all game languages and icons.
- /// Accessing this static property outside of is dangerous and not supported. - ///
- /// - /// A font handle corresponding to this font can be obtained with: - /// - /// fontAtlas.NewDelegateFontHandle( - /// e => e.OnPreBuild( - /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); - /// - /// public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; /// - /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt. ///
- /// - /// A font handle corresponding to this font can be obtained with: - /// - /// fontAtlas.NewDelegateFontHandle( - /// e => e.OnPreBuild( - /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); - /// - /// public static ImFontPtr IconFont => InterfaceManager.IconFont; /// - /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt. ///
- /// - /// A font handle corresponding to this font can be obtained with: - /// - /// fontAtlas.NewDelegateFontHandle( - /// e => e.OnPreBuild( - /// tk => tk.AddDalamudAssetFont( - /// DalamudAsset.InconsolataRegular, - /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); - /// - /// public static ImFontPtr MonoFont => InterfaceManager.MonoFont; /// @@ -250,11 +190,6 @@ public sealed class UiBuilder : IDisposable /// public bool UiPrepared => Service.GetNullable() != null; - /// - /// Gets the plugin-private font atlas. - /// - public IFontAtlas FontAtlas { get; } - /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// @@ -384,7 +319,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) + .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) .Unwrap(); } else @@ -406,7 +341,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) + .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) .Unwrap(); } else @@ -422,49 +357,19 @@ public sealed class UiBuilder : IDisposable ///
/// Font to get. /// Handle to the game font which may or may not be available for use yet. - [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] - public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( - (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), - Service.Get()); + public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style); /// /// Call this to queue a rebuild of the font atlas.
- /// This will invoke any and handlers and ensure that any - /// loaded fonts are ready to be used on the next UI frame. + /// This will invoke any handlers and ensure that any loaded fonts are + /// ready to be used on the next UI frame. ///
public void RebuildFonts() { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); - if (this.AfterBuildFonts is null && this.BuildFonts is null) - this.FontAtlas.BuildFontsAsync(); - else - this.FontAtlas.BuildFontsOnNextFrame(); + this.interfaceManager.RebuildFonts(); } - /// - /// Creates an isolated . - /// - /// Specify when and how to rebuild this atlas. - /// Whether the fonts in the atlas is global scaled. - /// Name for debugging purposes. - /// A new instance of . - /// - /// Use this to create extra font atlases, if you want to create and dispose fonts without having to rebuild all - /// other fonts together.
- /// If is not , - /// the font rebuilding functions must be called manually. - ///
- public IFontAtlas CreateFontAtlas( - FontAtlasAutoRebuildMode autoRebuildMode, - bool isGlobalScaled = true, - string? debugName = null) => - this.scopedFinalizer.Add(Service - .Get() - .CreateFontAtlas( - this.namespaceName + ":" + (debugName ?? "custom"), - autoRebuildMode, - isGlobalScaled)); - /// /// Add a notification to the notification queue. /// @@ -487,7 +392,12 @@ public sealed class UiBuilder : IDisposable /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); + void IDisposable.Dispose() + { + this.interfaceManager.Draw -= this.OnDraw; + this.interfaceManager.BuildFonts -= this.OnBuildFonts; + this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; + } /// /// Open the registered configuration UI, if it exists. @@ -553,12 +463,8 @@ public sealed class UiBuilder : IDisposable this.ShowUi?.InvokeSafely(); } - // just in case, if something goes wrong, prevent drawing; otherwise it probably will crash. - if (!this.FontAtlas.BuildTask.IsCompletedSuccessfully - && (this.BuildFonts is not null || this.AfterBuildFonts is not null)) - { + if (!this.interfaceManager.FontsReady) return; - } ImGui.PushID(this.namespaceName); if (DoStats) @@ -620,28 +526,14 @@ public sealed class UiBuilder : IDisposable this.hitchDetector.Stop(); } - private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e) + private void OnBuildFonts() { - if (e.IsAsyncBuildOperation) - return; + this.BuildFonts?.InvokeSafely(); + } - e.OnPreBuild( - _ => - { - var prev = ImGui.GetIO().NativePtr->Fonts; - ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; - this.BuildFonts?.InvokeSafely(); - ImGui.GetIO().NativePtr->Fonts = prev; - }); - - e.OnPostBuild( - _ => - { - var prev = ImGui.GetIO().NativePtr->Fonts; - ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; - this.AfterBuildFonts?.InvokeSafely(); - ImGui.GetIO().NativePtr->Fonts = prev; - }); + private void OnAfterBuildFonts() + { + this.AfterBuildFonts?.InvokeSafely(); } private void OnResizeBuffers() diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 444463d41..ad151ec4e 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,15 +1,10 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Numerics; -using System.Reactive.Disposables; using System.Runtime.InteropServices; -using System.Text.Unicode; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using ImGuiScene; @@ -36,7 +31,8 @@ public static class ImGuiHelpers /// This does not necessarily mean you can call drawing functions. /// public static unsafe bool IsImGuiInitialized => - ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null; + ImGui.GetCurrentContext() is not (nint)0 // KW: IDEs get mad without the cast, despite being unnecessary + && ImGui.GetIO().NativePtr is not null; /// /// Gets the global Dalamud scale; even available before drawing is ready.
@@ -202,7 +198,7 @@ public static class ImGuiHelpers /// If a positive number is given, numbers will be rounded to this. public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f) { - Func rounder = round > 0 ? x => MathF.Round(x / round) * round : x => x; + Func rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x; var font = fontPtr.NativePtr; font->FontSize = rounder(font->FontSize * scale); @@ -314,7 +310,6 @@ public static class ImGuiHelpers glyph->U1, glyph->V1, glyph->AdvanceX * scale); - target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint); changed = true; } else if (!missingOnly) @@ -348,18 +343,25 @@ public static class ImGuiHelpers } if (changed && rebuildLookupTable) - { - // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. - // FallbackGlyph is resolved after resolving ' '. - // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, - // making FindGlyph return nullptr. - // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, - // making ImGui attempt to treat whatever was there as a ' '. - // This may cause random glyphs to be sized randomly, if not an access violation exception. - target.NativePtr->FallbackGlyph = null; + target.BuildLookupTableNonstandard(); + } - target.BuildLookupTable(); - } + /// + /// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions. + /// + /// The font. + public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font) + { + // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. + // FallbackGlyph is resolved after resolving ' '. + // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, + // making FindGlyph return nullptr. + // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, + // making ImGui attempt to treat whatever was there as a ' '. + // This may cause random glyphs to be sized randomly, if not an access violation exception. + font.NativePtr->FallbackGlyph = null; + + font.BuildLookupTable(); } /// @@ -405,103 +407,6 @@ public static class ImGuiHelpers public static void CenterCursorFor(float itemWidth) => ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); - /// - /// Allocates memory on the heap using
- /// Memory must be freed using . - ///
- /// Note that null is a valid return value when is 0. - ///
- /// The length of allocated memory. - /// The allocated memory. - /// If returns null. - public static unsafe void* AllocateMemory(int length) - { - // TODO: igMemAlloc takes size_t, which is nint; ImGui.NET apparently interpreted that as uint. - // fix that in ImGui.NET. - switch (length) - { - case 0: - return null; - case < 0: - throw new ArgumentOutOfRangeException( - nameof(length), - length, - $"{nameof(length)} cannot be a negative number."); - default: - var memory = ImGuiNative.igMemAlloc((uint)length); - if (memory is null) - { - throw new OutOfMemoryException( - $"Failed to allocate {length} bytes using {nameof(ImGuiNative.igMemAlloc)}"); - } - - return memory; - } - } - - /// - /// Creates a new instance of with a natively backed memory. - /// - /// The created instance. - /// Disposable you can call. - public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder) - { - builder = new(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder()); - var ptr = builder.NativePtr; - return Disposable.Create(() => - { - if (ptr != null) - ImGuiNative.ImFontGlyphRangesBuilder_destroy(ptr); - ptr = null; - }); - } - - /// - /// Builds ImGui Glyph Ranges for use with . - /// - /// The builder. - /// Add fallback codepoints to the range. - /// Add ellipsis codepoints to the range. - /// When disposed, the resource allocated for the range will be freed. - public static unsafe ushort[] BuildRangesToArray( - this ImFontGlyphRangesBuilderPtr builder, - bool addFallbackCodepoints = true, - bool addEllipsisCodepoints = true) - { - if (addFallbackCodepoints) - builder.AddText(FontAtlasFactory.FallbackCodepoints); - if (addEllipsisCodepoints) - { - builder.AddText(FontAtlasFactory.EllipsisCodepoints); - builder.AddChar('.'); - } - - builder.BuildRanges(out var vec); - return new ReadOnlySpan((void*)vec.Data, vec.Size).ToArray(); - } - - /// - public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges) - => CreateImGuiRangesFrom((IEnumerable)ranges); - - /// - /// Creates glyph ranges from .
- /// Use values from . - ///
- /// The unicode ranges. - /// The range array that can be used for . - public static ushort[] CreateImGuiRangesFrom(IEnumerable ranges) => - ranges - .Where(x => x.FirstCodePoint <= ushort.MaxValue) - .SelectMany( - x => new[] - { - (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), - (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), - }) - .Append((ushort)0) - .ToArray(); - /// /// Determines whether is empty. /// @@ -510,7 +415,7 @@ public static class ImGuiHelpers public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; /// - /// Determines whether is empty. + /// Determines whether is not null and loaded. /// /// The pointer. /// Whether it is empty. @@ -522,27 +427,6 @@ public static class ImGuiHelpers /// The pointer. /// Whether it is empty. public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; - - /// - /// If is default, then returns . - /// - /// The self. - /// The other. - /// if it is not default; otherwise, . - public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => - self.NativePtr is null ? other : self; - - /// - /// Mark 4K page as used, after adding a codepoint to a font. - /// - /// The font. - /// The codepoint. - internal static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) - { - // Mark 4K page as used - var pageIndex = unchecked((ushort)(codepoint / 4096)); - font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); - } /// /// Finds the corresponding ImGui viewport ID for the given window handle. @@ -564,89 +448,6 @@ public static class ImGuiHelpers return -1; } - /// - /// Attempts to validate that is valid. - /// - /// The font pointer. - /// The exception, if any occurred during validation. - internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr) - { - try - { - var font = fontPtr.NativePtr; - if (font is null) - throw new NullReferenceException("The font is null."); - - _ = Marshal.ReadIntPtr((nint)font); - if (font->IndexedHotData.Data != 0) - _ = Marshal.ReadIntPtr(font->IndexedHotData.Data); - if (font->FrequentKerningPairs.Data != 0) - _ = Marshal.ReadIntPtr(font->FrequentKerningPairs.Data); - if (font->IndexLookup.Data != 0) - _ = Marshal.ReadIntPtr(font->IndexLookup.Data); - if (font->Glyphs.Data != 0) - _ = Marshal.ReadIntPtr(font->Glyphs.Data); - if (font->KerningPairs.Data != 0) - _ = Marshal.ReadIntPtr(font->KerningPairs.Data); - if (font->ConfigDataCount == 0 && font->ConfigData is not null) - throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?"); - if (font->ConfigDataCount != 0 && font->ConfigData is null) - throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?"); - if (font->ConfigData is not null) - _ = Marshal.ReadIntPtr((nint)font->ConfigData); - if (font->FallbackGlyph is not null - && ((nint)font->FallbackGlyph < font->Glyphs.Data || (nint)font->FallbackGlyph >= font->Glyphs.Data)) - throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); - if (font->FallbackHotData is not null - && ((nint)font->FallbackHotData < font->IndexedHotData.Data - || (nint)font->FallbackHotData >= font->IndexedHotData.Data)) - throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); - if (font->ContainerAtlas is not null) - _ = Marshal.ReadIntPtr((nint)font->ContainerAtlas); - } - catch (Exception e) - { - return e; - } - - return null; - } - - /// - /// Updates the fallback char of . - /// - /// The font. - /// The fallback character. - internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c) - { - font.FallbackChar = c; - font.NativePtr->FallbackHotData = - (ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar); - } - - /// - /// Determines if the supplied codepoint is inside the given range, - /// in format of . - /// - /// The codepoint. - /// The ranges. - /// Whether it is the case. - internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr) - { - if (codepoint is <= 0 or >= ushort.MaxValue) - return false; - - while (*rangePtr != 0) - { - var from = *rangePtr++; - var to = *rangePtr++; - if (from <= codepoint && codepoint <= to) - return true; - } - - return false; - } - /// /// Get data needed for each new frame. /// From b3740d0539e5a03d4a2a5c64c479be7c3ee10dd5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:03:14 +0100 Subject: [PATCH 25/71] add Profile.RemoveByInternalNameAsync() --- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- Dalamud/Plugin/Internal/Profiles/Profile.cs | 36 ++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 20e2ea7af..5d250a533 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1297,7 +1297,7 @@ internal partial class PluginManager : IDisposable, IServiceType try { // We don't need to apply, it doesn't matter - await this.profileManager.DefaultProfile.RemoveAsync(repoManifest.InternalName, false); + await this.profileManager.DefaultProfile.RemoveByInternalNameAsync(repoManifest.InternalName, false); } catch (ProfileOperationException) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 3e7a2ed55..36cafa29b 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -208,7 +208,7 @@ internal class Profile { entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); if (entry == null) - throw new PluginNotFoundException(workingPluginId.ToString()); + throw new PluginNotFoundException(workingPluginId); if (!this.modelV1.Plugins.Remove(entry)) throw new Exception("Couldn't remove plugin from model collection"); @@ -233,6 +233,31 @@ internal class Profile await this.manager.ApplyAllWantStatesAsync(); } + /// + /// Remove a plugin from this profile. + /// This will block until all states have been applied. + /// + /// The internal name of the plugin. + /// Whether or not the current state should immediately be applied. + /// A representing the asynchronous operation. + public async Task RemoveByInternalNameAsync(string internalName, bool apply = true) + { + Guid? pluginToRemove = null; + lock (this) + { + foreach (var plugin in this.Plugins) + { + if (plugin.InternalName.Equals(internalName, StringComparison.Ordinal)) + { + pluginToRemove = plugin.WorkingPluginId; + break; + } + } + } + + await this.RemoveAsync(pluginToRemove ?? throw new PluginNotFoundException(internalName), apply); + } + /// /// This function tries to migrate all plugins with this internalName which do not have /// a GUID to the specified GUID. @@ -308,4 +333,13 @@ internal sealed class PluginNotFoundException : ProfileOperationException : base($"The plugin '{internalName}' was not found in the profile") { } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the plugin causing the error. + public PluginNotFoundException(Guid workingPluginId) + : base($"The plugin '{workingPluginId}' was not found in the profile") + { + } } From d827151ee550cb0500690dd1ee56ca7c6f501f98 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:21:37 +0100 Subject: [PATCH 26/71] add icon for dev plugins --- Dalamud/DalamudAsset.cs | 19 ++++++---- .../Internal/Windows/PluginImageCache.cs | 6 ++++ .../PluginInstaller/PluginInstallerWindow.cs | 8 +++++ .../PluginInstaller/ProfileManagerWidget.cs | 35 ++++++++++++------- Dalamud/Logging/Internal/ModuleLog.cs | 10 +++--- 5 files changed, 55 insertions(+), 23 deletions(-) diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index 184193796..a7b35b196 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -63,41 +63,48 @@ public enum DalamudAsset [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "troubleIcon.png")] TroubleIcon = 1006, + + /// + /// : The plugin trouble icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "devPluginIcon.png")] + DevPluginIcon = 1007, /// /// : The plugin update icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "updateIcon.png")] - UpdateIcon = 1007, + UpdateIcon = 1008, /// /// : The plugin installed icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "installedIcon.png")] - InstalledIcon = 1008, + InstalledIcon = 1009, /// /// : The third party plugin icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "thirdIcon.png")] - ThirdIcon = 1009, + ThirdIcon = 1010, /// /// : The installed third party plugin icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "thirdInstalledIcon.png")] - ThirdInstalledIcon = 1010, + ThirdInstalledIcon = 1011, /// /// : The API bump explainer icon. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "changelogApiBump.png")] - ChangelogApiBumpIcon = 1011, + ChangelogApiBumpIcon = 1012, /// /// : The background shade for @@ -105,7 +112,7 @@ public enum DalamudAsset /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "tsmShade.png")] - TitleScreenMenuShade = 1012, + TitleScreenMenuShade = 1013, /// /// : Noto Sans CJK JP Medium. diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 528507229..29adbb3e5 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -98,6 +98,12 @@ internal class PluginImageCache : IDisposable, IServiceType /// public IDalamudTextureWrap TroubleIcon => this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TroubleIcon, this.EmptyTexture); + + /// + /// Gets the devPlugin icon overlay. + /// + public IDalamudTextureWrap DevPluginIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon, this.EmptyTexture); /// /// Gets the plugin update icon overlay. diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 6db48405d..240383695 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1800,6 +1800,14 @@ internal class PluginInstallerWindow : Window, IDisposable var isLoaded = plugin is { IsLoaded: true }; + if (plugin is LocalDevPlugin) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); + ImGui.Image(this.imageCache.DevPluginIcon.ImGuiHandle, iconSize); + ImGui.PopStyleVar(); + ImGui.SetCursorPos(cursorBeforeImage); + } + if (updateAvailable) ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); else if ((trouble && !pluginDisabled) || isOrphan) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index def5f8ce8..26006c84a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Profiles; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using ImGuiNET; using Serilog; @@ -315,13 +316,13 @@ internal class ProfileManagerWidget if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80))) { // TODO: Plugin searching should be abstracted... installer and this should use the same search - foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && !x.IsDev && + foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && (this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant())))) { using var disabled2 = ImRaii.Disabled(profile.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName)); - if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}")) + if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}")) { Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); @@ -426,18 +427,28 @@ internal class ProfileManagerWidget Guid? wantRemovePluginGuid = null; using var syncScope = profile.GetSyncScope(); - foreach (var plugin in profile.Plugins.ToArray()) + foreach (var profileEntry in profile.Plugins.ToArray()) { didAny = true; - var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.WorkingPluginId == plugin.WorkingPluginId); + var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.WorkingPluginId == profileEntry.WorkingPluginId); var btnOffset = 2; if (pmPlugin != null) { + var cursorBeforeIcon = ImGui.GetCursorPos(); pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon); icon ??= pic.DefaultIcon; ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight)); + + if (pmPlugin is LocalDevPlugin) + { + ImGui.SetCursorPos(cursorBeforeIcon); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); + ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight)); + ImGui.PopStyleVar(); + } + ImGui.SameLine(); var text = $"{pmPlugin.Name}"; @@ -454,17 +465,17 @@ internal class ProfileManagerWidget ImGui.Image(pic.DefaultIcon.ImGuiHandle, new Vector2(pluginLineHeight)); ImGui.SameLine(); - var text = Locs.NotInstalled(plugin.InternalName); + var text = Locs.NotInstalled(profileEntry.InternalName); var textHeight = ImGui.CalcTextSize(text); var before = ImGui.GetCursorPos(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); ImGui.TextUnformatted(text); - var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == plugin.InternalName); + var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == profileEntry.InternalName); var installable = pm.AvailablePlugins.FirstOrDefault( - x => x.InternalName == plugin.InternalName && !x.SourceRepo.IsThirdParty); + x => x.InternalName == profileEntry.InternalName && !x.SourceRepo.IsThirdParty); if (firstAvailableInstalled != null) { @@ -494,10 +505,10 @@ internal class ProfileManagerWidget ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30)); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); - var enabled = plugin.IsEnabled; - if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled)) + var enabled = profileEntry.IsEnabled; + if (ImGui.Checkbox($"###{this.editingProfileGuid}-{profileEntry.InternalName}", ref enabled)) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.WorkingPluginId, enabled)) + Task.Run(() => profile.AddOrUpdateAsync(profileEntry.WorkingPluginId, enabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } @@ -505,9 +516,9 @@ internal class ProfileManagerWidget ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * btnOffset) - 5); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); - if (ImGuiComponents.IconButton($"###removePlugin{plugin.InternalName}", FontAwesomeIcon.Trash)) + if (ImGuiComponents.IconButton($"###removePlugin{profileEntry.InternalName}", FontAwesomeIcon.Trash)) { - wantRemovePluginGuid = plugin.WorkingPluginId; + wantRemovePluginGuid = profileEntry.WorkingPluginId; } if (ImGui.IsItemHovered()) diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index e59db09d3..1fe955294 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -43,7 +43,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Verbose(Exception exception, string messageTemplate, params object?[] values) + public void Verbose(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, exception, values); /// @@ -62,7 +62,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Debug(Exception exception, string messageTemplate, params object?[] values) + public void Debug(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, exception, values); /// @@ -81,7 +81,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Information(Exception exception, string messageTemplate, params object?[] values) + public void Information(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, exception, values); /// @@ -100,7 +100,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Warning(Exception exception, string messageTemplate, params object?[] values) + public void Warning(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, exception, values); /// @@ -138,7 +138,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Fatal(Exception exception, string messageTemplate, params object?[] values) + public void Fatal(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); [MessageTemplateFormatMethod("messageTemplate")] From 256f4989f7795a15f47badcb3f4b8e2bb0e628db Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:39:18 +0100 Subject: [PATCH 27/71] add some validation code to catch issues --- Dalamud/Plugin/Internal/PluginManager.cs | 33 +++++++++++++++++++ .../Internal/Profiles/ProfileManager.cs | 31 +++++++++++++---- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 5d250a533..7af530ee9 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -664,6 +664,15 @@ internal partial class PluginManager : IDisposable, IServiceType this.PluginsReady = true; this.NotifyinstalledPluginsListChanged(); sigScanner.Save(); + + try + { + this.ParanoiaValidatePluginsAndProfiles(); + } + catch (Exception ex) + { + Log.Error(ex, "Plugin and profile validation failed!"); + } }, tokenSource.Token); } @@ -1256,6 +1265,30 @@ internal partial class PluginManager : IDisposable, IServiceType } } + /// + /// Check if there are any inconsistencies with our plugins, their IDs, and our profiles. + /// + private void ParanoiaValidatePluginsAndProfiles() + { + var seenIds = new List(); + + foreach (var installedPlugin in this.InstalledPlugins) + { + if (installedPlugin.Manifest.WorkingPluginId == Guid.Empty) + throw new Exception($"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has an empty WorkingPluginId."); + + if (seenIds.Contains(installedPlugin.Manifest.WorkingPluginId)) + { + throw new Exception( + $"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has a duplicate WorkingPluginId '{installedPlugin.Manifest.WorkingPluginId}'"); + } + + seenIds.Add(installedPlugin.Manifest.WorkingPluginId); + } + + this.profileManager.ParanoiaValidateProfiles(); + } + private async Task DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting) { var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 6b51f7535..768583bea 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -216,19 +216,18 @@ internal class ProfileManager : IServiceType this.isBusy = true; Log.Information("Getting want states..."); - List wantActive; + List wantActive; lock (this.profiles) { wantActive = this.profiles .Where(x => x.IsEnabled) - .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled) - .Select(plugin => plugin.WorkingPluginId)) + .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled)) .Distinct().ToList(); } - foreach (var internalName in wantActive) + foreach (var profilePluginEntry in wantActive) { - Log.Information("\t=> Want {Name}", internalName); + Log.Information("\t=> Want {Name}({WorkingPluginId})", profilePluginEntry.InternalName, profilePluginEntry.WorkingPluginId); } Log.Information("Applying want states..."); @@ -238,7 +237,7 @@ internal class ProfileManager : IServiceType var pm = Service.Get(); foreach (var installedPlugin in pm.InstalledPlugins) { - var wantThis = wantActive.Contains(installedPlugin.Manifest.WorkingPluginId); + var wantThis = wantActive.Any(x => x.WorkingPluginId == installedPlugin.Manifest.WorkingPluginId); switch (wantThis) { case true when !installedPlugin.IsLoaded: @@ -314,6 +313,26 @@ internal class ProfileManager : IServiceType profile.MigrateProfilesToGuidsForPlugin(internalName, newGuid); } } + + /// + /// Validate profiles for errors. + /// + /// Thrown when a profile is not sane. + public void ParanoiaValidateProfiles() + { + foreach (var profile in this.profiles) + { + var seenIds = new List(); + + foreach (var pluginEntry in profile.Plugins) + { + if (seenIds.Contains(pluginEntry.WorkingPluginId)) + throw new Exception($"Plugin '{pluginEntry.WorkingPluginId}'('{pluginEntry.InternalName}') is twice in profile '{profile.Guid}'('{profile.Name}')"); + + seenIds.Add(pluginEntry.WorkingPluginId); + } + } + } private string GenerateUniqueProfileName(string startingWith) { From 9024c9b00c8ec826c47468f01a8485647af3fb84 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:47:56 +0100 Subject: [PATCH 28/71] track internal name nonetheless --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 10 +++++----- .../Windows/PluginInstaller/ProfileManagerWidget.cs | 4 ++-- Dalamud/Plugin/Internal/PluginManager.cs | 10 +++++----- Dalamud/Plugin/Internal/Profiles/Profile.cs | 6 ++++-- Dalamud/Plugin/Internal/Profiles/ProfileManager.cs | 7 ++++--- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 240383695..1545efb65 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2579,7 +2579,7 @@ internal class PluginInstallerWindow : Window, IDisposable { if (inProfile) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd); } else @@ -2604,7 +2604,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { // TODO: Work this out - Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.IsLoaded, false)) + Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, plugin.IsLoaded, false)) .GetAwaiter().GetResult(); foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName))) { @@ -2682,7 +2682,7 @@ internal class PluginInstallerWindow : Window, IDisposable { await plugin.UnloadAsync(); await applicableProfile.AddOrUpdateAsync( - plugin.Manifest.WorkingPluginId, false, false); + plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success); }).ContinueWith(t => @@ -2699,7 +2699,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle; this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId; - await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false); await plugin.LoadAsync(PluginLoadReason.Installer); notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success); @@ -2720,7 +2720,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (shouldUpdate) { // We need to update the profile right here, because PM will not enable the plugin otherwise - await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false); await this.UpdateSinglePlugin(availableUpdate); } else diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 26006c84a..62806404a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -324,7 +324,7 @@ internal class ProfileManagerWidget if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}")) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } } @@ -508,7 +508,7 @@ internal class ProfileManagerWidget var enabled = profileEntry.IsEnabled; if (ImGui.Checkbox($"###{this.editingProfileGuid}-{profileEntry.InternalName}", ref enabled)) { - Task.Run(() => profile.AddOrUpdateAsync(profileEntry.WorkingPluginId, enabled)) + Task.Run(() => profile.AddOrUpdateAsync(profileEntry.WorkingPluginId, profileEntry.InternalName, enabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 7af530ee9..c57487d1d 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1511,28 +1511,28 @@ internal partial class PluginManager : IDisposable, IServiceType { // We didn't want this plugin, and StartOnBoot is on. That means we don't want it and it should stay off until manually enabled. Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot) { // We wanted this plugin, and StartOnBoot is on. That means we actually do want it. Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false); loadPlugin = !doNotLoad; } else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot) { // We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore. Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot) { // We didn't want this plugin, and StartOnBoot is off. We don't want it. Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); loadPlugin = false; } @@ -1544,7 +1544,7 @@ internal partial class PluginManager : IDisposable, IServiceType #pragma warning restore CS0618 // Need to do this here, so plugins that don't load are still added to the default profile - var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, defaultState); + var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); if (loadPlugin) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 36cafa29b..df5b045e2 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -158,10 +158,11 @@ internal class Profile /// This will block until all states have been applied. /// /// The ID of the plugin. + /// The internal name of the plugin, if available. /// Whether or not the plugin should be enabled. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. - public async Task AddOrUpdateAsync(Guid workingPluginId, bool state, bool apply = true) + public async Task AddOrUpdateAsync(Guid workingPluginId, string? internalName, bool state, bool apply = true) { Debug.Assert(workingPluginId != Guid.Empty, "Trying to add plugin with empty guid"); @@ -176,6 +177,7 @@ internal class Profile { this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin { + InternalName = internalName, WorkingPluginId = workingPluginId, IsEnabled = state, }); @@ -219,7 +221,7 @@ internal class Profile { if (!this.IsDefaultProfile) { - await this.manager.DefaultProfile.AddOrUpdateAsync(workingPluginId, this.IsEnabled && entry.IsEnabled, false); + await this.manager.DefaultProfile.AddOrUpdateAsync(workingPluginId, entry.InternalName, this.IsEnabled && entry.IsEnabled, false); } else { diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 768583bea..10d94de73 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -70,10 +70,11 @@ internal class ProfileManager : IServiceType /// Check if any enabled profile wants a specific plugin enabled. /// /// The ID of the plugin. + /// The internal name of the plugin, if available. /// The state the plugin shall be in, if it needs to be added. /// Whether or not the plugin should be added to the default preset, if it's not present in any preset. /// Whether or not the plugin shall be enabled. - public async Task GetWantStateAsync(Guid workingPluginId, bool defaultState, bool addIfNotDeclared = true) + public async Task GetWantStateAsync(Guid workingPluginId, string? internalName, bool defaultState, bool addIfNotDeclared = true) { var want = false; var wasInAnyProfile = false; @@ -93,8 +94,8 @@ internal class ProfileManager : IServiceType if (!wasInAnyProfile && addIfNotDeclared) { - Log.Warning("{Guid} was not in any profile, adding to default with {Default}", workingPluginId, defaultState); - await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, defaultState, false); + Log.Warning("'{Guid}'('{InternalName}') was not in any profile, adding to default with {Default}", workingPluginId, internalName, defaultState); + await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, internalName, defaultState, false); return defaultState; } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 348563781..0f65bafb2 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -164,7 +164,7 @@ internal class LocalPlugin : IDisposable /// INCLUDES the default profile. /// public bool IsWantedByAnyProfile => - Service.Get().GetWantStateAsync(this.manifest.WorkingPluginId, false, false).GetAwaiter().GetResult(); + Service.Get().GetWantStateAsync(this.manifest.WorkingPluginId, this.Manifest.InternalName, false, false).GetAwaiter().GetResult(); /// /// Gets a value indicating whether this plugin's API level is out of date. From 23ddc7824126efccc226e36d6a8cf328a992757a Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:53:17 +0100 Subject: [PATCH 29/71] add bodge "match to plugin" UI for installed plugins --- .../PluginInstaller/ProfileManagerWidget.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 62806404a..2d45869e0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -479,8 +479,22 @@ internal class ProfileManagerWidget if (firstAvailableInstalled != null) { - // TODO - ImGui.Text("GOAT WAS TOO LAZY TO IMPLEMENT THIS"); + ImGui.Text($"Match to plugin '{firstAvailableInstalled.Name}'?"); + ImGui.SameLine(); + if (ImGuiComponents.IconButtonWithText( + FontAwesomeIcon.Check, + "Yes, use this one")) + { + profileEntry.WorkingPluginId = firstAvailableInstalled.Manifest.WorkingPluginId; + Task.Run(async () => + { + await profman.ApplyAllWantStatesAsync(); + }) + .ContinueWith(t => + { + this.installer.DisplayErrorContinuation(t, Locs.ErrorCouldNotChangeState); + }); + } } else if (installable != null) { From 63b16bcc7cd5fba67fbe9943caae92f851e05441 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 19 Jan 2024 07:26:56 +0900 Subject: [PATCH 30/71] Reapply "IFontAtlas: font atlas per plugin" This reverts commit b5696afe94b9ace8c58323a751b5bb88cae9cece. --- .../Internal/DalamudConfiguration.cs | 7 +- Dalamud/Interface/GameFonts/FdtFileView.cs | 159 ++ .../GameFonts/GameFontFamilyAndSize.cs | 25 +- .../GameFontFamilyAndSizeAttribute.cs | 37 + Dalamud/Interface/GameFonts/GameFontHandle.cs | 83 +- .../Interface/GameFonts/GameFontManager.cs | 507 ------ Dalamud/Interface/GameFonts/GameFontStyle.cs | 37 +- Dalamud/Interface/Internal/DalamudIme.cs | 7 +- .../Interface/Internal/DalamudInterface.cs | 13 +- .../Interface/Internal/InterfaceManager.cs | 964 +++--------- .../Internal/Windows/ChangelogWindow.cs | 62 +- .../Internal/Windows/Data/DataWindow.cs | 8 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 213 +++ .../Windows/Settings/SettingsWindow.cs | 29 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 30 +- .../Windows/Settings/Tabs/SettingsTabLook.cs | 50 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 63 +- .../FontAtlasAutoRebuildMode.cs | 22 + .../ManagedFontAtlas/FontAtlasBuildStep.cs | 38 + .../FontAtlasBuildStepDelegate.cs | 15 + .../FontAtlasBuildToolkitUtilities.cs | 133 ++ .../Interface/ManagedFontAtlas/IFontAtlas.cs | 141 ++ .../IFontAtlasBuildToolkit.cs | 67 + .../IFontAtlasBuildToolkitPostBuild.cs | 26 + .../IFontAtlasBuildToolkitPostPromotion.cs | 33 + .../IFontAtlasBuildToolkitPreBuild.cs | 186 +++ .../Interface/ManagedFontAtlas/IFontHandle.cs | 42 + .../Internals/DelegateFontHandle.cs | 334 ++++ .../FontAtlasFactory.BuildToolkit.cs | 682 ++++++++ .../FontAtlasFactory.Implementation.cs | 726 +++++++++ .../Internals/FontAtlasFactory.cs | 368 +++++ .../Internals/GamePrebakedFontHandle.cs | 857 ++++++++++ .../Internals/IFontHandleManager.cs | 32 + .../Internals/IFontHandleSubstance.cs | 54 + .../Internals/TrueType.Common.cs | 203 +++ .../Internals/TrueType.Enums.cs | 84 + .../Internals/TrueType.Files.cs | 148 ++ .../Internals/TrueType.GposGsub.cs | 259 +++ .../Internals/TrueType.PointerSpan.cs | 443 ++++++ .../Internals/TrueType.Tables.cs | 1391 +++++++++++++++++ .../ManagedFontAtlas/Internals/TrueType.cs | 135 ++ .../ManagedFontAtlas/SafeFontConfig.cs | 306 ++++ Dalamud/Interface/UiBuilder.cs | 182 ++- Dalamud/Interface/Utility/ImGuiHelpers.cs | 243 ++- 44 files changed, 7944 insertions(+), 1500 deletions(-) create mode 100644 Dalamud/Interface/GameFonts/FdtFileView.cs create mode 100644 Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs delete mode 100644 Dalamud/Interface/GameFonts/GameFontManager.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 76c8f3603..66c2745c5 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -148,12 +148,9 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable public bool UseAxisFontsFromGame { get; set; } = false; /// - /// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness. - /// - /// Before gamma is applied... - /// * ...TTF fonts loaded with stb or FreeType are in linear space. - /// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4. + /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use. /// + [Obsolete("It happens that nobody touched this setting", true)] public float FontGammaLevel { get; set; } = 1.4f; /// diff --git a/Dalamud/Interface/GameFonts/FdtFileView.cs b/Dalamud/Interface/GameFonts/FdtFileView.cs new file mode 100644 index 000000000..896a6dbb4 --- /dev/null +++ b/Dalamud/Interface/GameFonts/FdtFileView.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.IO; + +namespace Dalamud.Interface.GameFonts; + +/// +/// Reference member view of a .fdt file data. +/// +internal readonly unsafe struct FdtFileView +{ + private readonly byte* ptr; + + /// + /// Initializes a new instance of the struct. + /// + /// Pointer to the data. + /// Length of the data. + public FdtFileView(void* ptr, int length) + { + this.ptr = (byte*)ptr; + if (length < sizeof(FdtReader.FdtHeader)) + throw new InvalidDataException("Not enough space for a FdtHeader"); + + if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader)) + throw new InvalidDataException("Not enough space for a FontTableHeader"); + if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader) + + (sizeof(FdtReader.FontTableEntry) * this.FontHeader.FontTableEntryCount)) + throw new InvalidDataException("Not enough space for all the FontTableEntry"); + + if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader)) + throw new InvalidDataException("Not enough space for a KerningTableHeader"); + if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader) + + (sizeof(FdtReader.KerningTableEntry) * this.KerningEntryCount)) + throw new InvalidDataException("Not enough space for all the KerningTableEntry"); + } + + /// + /// Gets the file header. + /// + public ref FdtReader.FdtHeader FileHeader => ref *(FdtReader.FdtHeader*)this.ptr; + + /// + /// Gets the font header. + /// + public ref FdtReader.FontTableHeader FontHeader => + ref *(FdtReader.FontTableHeader*)((nint)this.ptr + this.FileHeader.FontTableHeaderOffset); + + /// + /// Gets the glyphs. + /// + public Span Glyphs => new(this.GlyphsUnsafe, this.FontHeader.FontTableEntryCount); + + /// + /// Gets the kerning header. + /// + public ref FdtReader.KerningTableHeader KerningHeader => + ref *(FdtReader.KerningTableHeader*)((nint)this.ptr + this.FileHeader.KerningTableHeaderOffset); + + /// + /// Gets the number of kerning entries. + /// + public int KerningEntryCount => Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); + + /// + /// Gets the kerning entries. + /// + public Span PairAdjustments => new( + this.ptr + this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader), + this.KerningEntryCount); + + /// + /// Gets the maximum texture index. + /// + public int MaxTextureIndex + { + get + { + var i = 0; + foreach (ref var g in this.Glyphs) + { + if (g.TextureIndex > i) + i = g.TextureIndex; + } + + return i; + } + } + + private FdtReader.FontTableEntry* GlyphsUnsafe => + (FdtReader.FontTableEntry*)(this.ptr + this.FileHeader.FontTableHeaderOffset + + sizeof(FdtReader.FontTableHeader)); + + /// + /// Finds the glyph index for the corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding index, or a negative number according to . + public int FindGlyphIndex(int codepoint) + { + var comp = FdtReader.CodePointToUtf8Int32(codepoint); + + var glyphs = this.GlyphsUnsafe; + var lo = 0; + var hi = this.FontHeader.FontTableEntryCount - 1; + while (lo <= hi) + { + var i = (int)(((uint)hi + (uint)lo) >> 1); + switch (comp.CompareTo(glyphs[i].CharUtf8)) + { + case 0: + return i; + case > 0: + lo = i + 1; + break; + default: + hi = i - 1; + break; + } + } + + return ~lo; + } + + /// + /// Create a glyph range for use with . + /// + /// Merge two ranges into one if distance is below the value specified in this parameter. + /// Glyph ranges. + public ushort[] ToGlyphRanges(int mergeDistance = 8) + { + var glyphs = this.Glyphs; + var ranges = new List(glyphs.Length) + { + checked((ushort)glyphs[0].CharInt), + checked((ushort)glyphs[0].CharInt), + }; + + foreach (ref var glyph in glyphs[1..]) + { + var c32 = glyph.CharInt; + if (c32 >= 0x10000) + break; + + var c16 = unchecked((ushort)c32); + if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) + { + ranges[^1] = c16; + } + else if (ranges[^1] + 1 < c16) + { + ranges.Add(c16); + ranges.Add(c16); + } + } + + ranges.Add(0); + return ranges.ToArray(); + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs index dd78baf87..6e66cf19b 100644 --- a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs @@ -3,7 +3,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Enum of available game fonts in specific sizes. /// -public enum GameFontFamilyAndSize : int +public enum GameFontFamilyAndSize { /// /// Placeholder meaning unused. @@ -15,6 +15,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_96.fdt", "common/font/font{0}.tex", -1)] Axis96, /// @@ -22,6 +23,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_12.fdt", "common/font/font{0}.tex", -1)] Axis12, /// @@ -29,6 +31,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_14.fdt", "common/font/font{0}.tex", -1)] Axis14, /// @@ -36,6 +39,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_18.fdt", "common/font/font{0}.tex", -1)] Axis18, /// @@ -43,6 +47,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_36.fdt", "common/font/font{0}.tex", -4)] Axis36, /// @@ -50,6 +55,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_16.fdt", "common/font/font{0}.tex", -1)] Jupiter16, /// @@ -57,6 +63,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_20.fdt", "common/font/font{0}.tex", -1)] Jupiter20, /// @@ -64,6 +71,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_23.fdt", "common/font/font{0}.tex", -1)] Jupiter23, /// @@ -71,6 +79,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// + [GameFontFamilyAndSize("common/font/Jupiter_45.fdt", "common/font/font{0}.tex", -2)] Jupiter45, /// @@ -78,6 +87,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_46.fdt", "common/font/font{0}.tex", -2)] Jupiter46, /// @@ -85,6 +95,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// + [GameFontFamilyAndSize("common/font/Jupiter_90.fdt", "common/font/font{0}.tex", -4)] Jupiter90, /// @@ -92,6 +103,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_16.fdt", "common/font/font{0}.tex", -1)] Meidinger16, /// @@ -99,6 +111,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_20.fdt", "common/font/font{0}.tex", -1)] Meidinger20, /// @@ -106,6 +119,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_40.fdt", "common/font/font{0}.tex", -4)] Meidinger40, /// @@ -113,6 +127,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_10.fdt", "common/font/font{0}.tex", -1)] MiedingerMid10, /// @@ -120,6 +135,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_12.fdt", "common/font/font{0}.tex", -1)] MiedingerMid12, /// @@ -127,6 +143,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_14.fdt", "common/font/font{0}.tex", -1)] MiedingerMid14, /// @@ -134,6 +151,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_18.fdt", "common/font/font{0}.tex", -1)] MiedingerMid18, /// @@ -141,6 +159,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_36.fdt", "common/font/font{0}.tex", -2)] MiedingerMid36, /// @@ -148,6 +167,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_184.fdt", "common/font/font{0}.tex", -1)] TrumpGothic184, /// @@ -155,6 +175,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_23.fdt", "common/font/font{0}.tex", -1)] TrumpGothic23, /// @@ -162,6 +183,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_34.fdt", "common/font/font{0}.tex", -1)] TrumpGothic34, /// @@ -169,5 +191,6 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_68.fdt", "common/font/font{0}.tex", -3)] TrumpGothic68, } diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs new file mode 100644 index 000000000..f5260e4bc --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs @@ -0,0 +1,37 @@ +namespace Dalamud.Interface.GameFonts; + +/// +/// Marks the path for an enum value. +/// +[AttributeUsage(AttributeTargets.Field)] +internal class GameFontFamilyAndSizeAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Inner path of the file. + /// the file path format for the relevant .tex files. + /// Horizontal offset of the corresponding font. + public GameFontFamilyAndSizeAttribute(string path, string texPathFormat, int horizontalOffset) + { + this.Path = path; + this.TexPathFormat = texPathFormat; + this.HorizontalOffset = horizontalOffset; + } + + /// + /// Gets the path. + /// + public string Path { get; } + + /// + /// Gets the file path format for the relevant .tex files.
+ /// Used for (, ). + ///
+ public string TexPathFormat { get; } + + /// + /// Gets the horizontal offset of the corresponding font. + /// + public int HorizontalOffset { get; } +} diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index d71e725c5..77461aa0a 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,75 +1,76 @@ -using System; using System.Numerics; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; + using ImGuiNET; namespace Dalamud.Interface.GameFonts; /// -/// Prepare and keep game font loaded for use in OnDraw. +/// ABI-compatible wrapper for . /// -public class GameFontHandle : IDisposable +public sealed class GameFontHandle : IFontHandle { - private readonly GameFontManager manager; - private readonly GameFontStyle fontStyle; + private readonly IFontHandle.IInternal fontHandle; + private readonly FontAtlasFactory fontAtlasFactory; /// /// Initializes a new instance of the class. /// - /// GameFontManager instance. - /// Font to use. - internal GameFontHandle(GameFontManager manager, GameFontStyle font) + /// The wrapped . + /// An instance of . + internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory) { - this.manager = manager; - this.fontStyle = font; + this.fontHandle = fontHandle; + this.fontAtlasFactory = fontAtlasFactory; } - /// - /// Gets the font style. - /// - public GameFontStyle Style => this.fontStyle; + /// + public Exception? LoadException => this.fontHandle.LoadException; + + /// + public bool Available => this.fontHandle.Available; + + /// + [Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)] + public ImFontPtr ImFont => this.fontHandle.ImFont; /// - /// Gets a value indicating whether this font is ready for use. + /// Gets the font style. Only applicable for . /// - public bool Available - { - get - { - unsafe - { - return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null; - } - } - } + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public GameFontStyle Style => ((GamePrebakedFontHandle)this.fontHandle).FontStyle; /// - /// Gets the font. + /// Gets the relevant .
+ ///
+ /// Only applicable for game fonts. Otherwise it will throw. ///
- public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value; + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public FdtReader FdtReader => this.fontAtlasFactory.GetFdtReader(this.Style.FamilyAndSize)!; + + /// + public void Dispose() => this.fontHandle.Dispose(); + + /// + public IDisposable Push() => this.fontHandle.Push(); /// - /// Gets the FdtReader. - /// - public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); - - /// - /// Creates a new GameFontLayoutPlan.Builder. + /// Creates a new .
+ ///
+ /// Only applicable for game fonts. Otherwise it will throw. ///
/// Text. /// A new builder for GameFontLayoutPlan. - public GameFontLayoutPlan.Builder LayoutBuilder(string text) - { - return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text); - } - - /// - public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public GameFontLayoutPlan.Builder LayoutBuilder(string text) => new(this.ImFont, this.FdtReader, text); /// /// Draws text. /// /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void Text(string text) { if (!this.Available) @@ -93,6 +94,7 @@ public class GameFontHandle : IDisposable ///
/// Color. /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextColored(Vector4 col, string text) { ImGui.PushStyleColor(ImGuiCol.Text, col); @@ -104,6 +106,7 @@ public class GameFontHandle : IDisposable /// Draws disabled text. ///
/// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextDisabled(string text) { unsafe diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs deleted file mode 100644 index b3454e085..000000000 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ /dev/null @@ -1,507 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Utility.Timing; -using ImGuiNET; -using Lumina.Data.Files; -using Serilog; - -using static Dalamud.Interface.Utility.ImGuiHelpers; - -namespace Dalamud.Interface.GameFonts; - -/// -/// Loads game font for use in ImGui. -/// -[ServiceManager.BlockingEarlyLoadedService] -internal class GameFontManager : IServiceType -{ - private static readonly string?[] FontNames = - { - null, - "AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36", - "Jupiter_16", "Jupiter_20", "Jupiter_23", "Jupiter_45", "Jupiter_46", "Jupiter_90", - "Meidinger_16", "Meidinger_20", "Meidinger_40", - "MiedingerMid_10", "MiedingerMid_12", "MiedingerMid_14", "MiedingerMid_18", "MiedingerMid_36", - "TrumpGothic_184", "TrumpGothic_23", "TrumpGothic_34", "TrumpGothic_68", - }; - - private readonly object syncRoot = new(); - - private readonly FdtReader?[] fdts; - private readonly List texturePixels; - private readonly Dictionary fonts = new(); - private readonly Dictionary fontUseCounter = new(); - private readonly Dictionary>> glyphRectIds = new(); - -#pragma warning disable CS0414 - private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; -#pragma warning restore CS0414 - - [ServiceManager.ServiceConstructor] - private GameFontManager(DataManager dataManager) - { - using (Timings.Start("Getting fdt data")) - { - this.fdts = FontNames.Select(fontName => fontName == null ? null : new FdtReader(dataManager.GetFile($"common/font/{fontName}.fdt")!.Data)).ToArray(); - } - - using (Timings.Start("Getting texture data")) - { - var texTasks = Enumerable - .Range(1, 1 + this.fdts - .Where(x => x != null) - .Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max()) - .Max()) - .Select(x => dataManager.GetFile($"common/font/font{x}.tex")!) - .Select(x => new Task(Timings.AttachTimingHandle(() => x.ImageData!))) - .ToArray(); - foreach (var task in texTasks) - task.Start(); - this.texturePixels = texTasks.Select(x => x.GetAwaiter().GetResult()).ToList(); - } - } - - /// - /// Describe font into a string. - /// - /// Font to describe. - /// A string in a form of "FontName (NNNpt)". - public static string DescribeFont(GameFontFamilyAndSize font) - { - return font switch - { - GameFontFamilyAndSize.Undefined => "-", - GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)", - GameFontFamilyAndSize.Axis12 => "AXIS (12pt)", - GameFontFamilyAndSize.Axis14 => "AXIS (14pt)", - GameFontFamilyAndSize.Axis18 => "AXIS (18pt)", - GameFontFamilyAndSize.Axis36 => "AXIS (36pt)", - GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)", - GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)", - GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)", - GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)", - GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)", - GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)", - GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)", - GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)", - GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)", - GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)", - GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)", - GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)", - GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)", - GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)", - GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)", - GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)", - GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)", - GameFontFamilyAndSize.TrumpGothic68 => "Trump Gothic (68pt)", - _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), - }; - } - - /// - /// Determines whether a font should be able to display most of stuff. - /// - /// Font to check. - /// True if it can. - public static bool IsGenericPurposeFont(GameFontFamilyAndSize font) - { - return font switch - { - GameFontFamilyAndSize.Axis96 => true, - GameFontFamilyAndSize.Axis12 => true, - GameFontFamilyAndSize.Axis14 => true, - GameFontFamilyAndSize.Axis18 => true, - GameFontFamilyAndSize.Axis36 => true, - _ => false, - }; - } - - /// - /// Unscales fonts after they have been rendered onto atlas. - /// - /// Font to unscale. - /// Scale factor. - /// Whether to call target.BuildLookupTable(). - public static void UnscaleFont(ImFontPtr fontPtr, float fontScale, bool rebuildLookupTable = true) - { - if (fontScale == 1) - return; - - unsafe - { - var font = fontPtr.NativePtr; - for (int i = 0, i_ = font->IndexedHotData.Size; i < i_; ++i) - { - font->IndexedHotData.Ref(i).AdvanceX /= fontScale; - font->IndexedHotData.Ref(i).OccupiedWidth /= fontScale; - } - - font->FontSize /= fontScale; - font->Ascent /= fontScale; - font->Descent /= fontScale; - if (font->ConfigData != null) - font->ConfigData->SizePixels /= fontScale; - var glyphs = (ImFontGlyphReal*)font->Glyphs.Data; - for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++) - { - var glyph = &glyphs[i]; - glyph->X0 /= fontScale; - glyph->X1 /= fontScale; - glyph->Y0 /= fontScale; - glyph->Y1 /= fontScale; - glyph->AdvanceX /= fontScale; - } - - for (int i = 0, i_ = font->KerningPairs.Size; i < i_; i++) - font->KerningPairs.Ref(i).AdvanceXAdjustment /= fontScale; - for (int i = 0, i_ = font->FrequentKerningPairs.Size; i < i_; i++) - font->FrequentKerningPairs.Ref(i) /= fontScale; - } - - if (rebuildLookupTable && fontPtr.Glyphs.Size > 0) - fontPtr.BuildLookupTableNonstandard(); - } - - /// - /// Create a glyph range for use with ImGui AddFont. - /// - /// Font family and size. - /// Merge two ranges into one if distance is below the value specified in this parameter. - /// Glyph ranges. - public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8) - { - var fdt = this.fdts[(int)family]!; - var ranges = new List(fdt.Glyphs.Count) - { - checked((ushort)fdt.Glyphs[0].CharInt), - checked((ushort)fdt.Glyphs[0].CharInt), - }; - - foreach (var glyph in fdt.Glyphs.Skip(1)) - { - var c32 = glyph.CharInt; - if (c32 >= 0x10000) - break; - - var c16 = unchecked((ushort)c32); - if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) - { - ranges[^1] = c16; - } - else if (ranges[^1] + 1 < c16) - { - ranges.Add(c16); - ranges.Add(c16); - } - } - - return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned); - } - - /// - /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. - /// - /// Font to use. - /// Handle to game font that may or may not be ready yet. - public GameFontHandle NewFontRef(GameFontStyle style) - { - var interfaceManager = Service.Get(); - var needRebuild = false; - - lock (this.syncRoot) - { - this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1; - } - - needRebuild = !this.fonts.ContainsKey(style); - if (needRebuild) - { - Log.Information("[GameFontManager] NewFontRef: Queueing RebuildFonts because {0} has been requested.", style.ToString()); - Service.GetAsync() - .ContinueWith(task => task.Result.RunOnTick(() => interfaceManager.RebuildFonts())); - } - - return new(this, style); - } - - /// - /// Gets the font. - /// - /// Font to get. - /// Corresponding font or null. - public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null); - - /// - /// Gets the corresponding FdtReader. - /// - /// Font to get. - /// Corresponding FdtReader or null. - public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family]; - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable); - } - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable); - } - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); - } - - /// - /// Build fonts before plugins do something more. To be called from InterfaceManager. - /// - public void BuildFonts() - { - this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = true; - - this.glyphRectIds.Clear(); - this.fonts.Clear(); - - lock (this.syncRoot) - { - foreach (var style in this.fontUseCounter.Keys) - this.EnsureFont(style); - } - } - - /// - /// Record that ImGui.GetIO().Fonts.Build() has been called. - /// - public void AfterIoFontsBuild() - { - this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; - } - - /// - /// Checks whether GameFontMamager owns an ImFont. - /// - /// ImFontPtr to check. - /// Whether it owns. - public bool OwnsFont(ImFontPtr fontPtr) => this.fonts.ContainsValue(fontPtr); - - /// - /// Post-build fonts before plugins do something more. To be called from InterfaceManager. - /// - public unsafe void AfterBuildFonts() - { - var interfaceManager = Service.Get(); - var ioFonts = ImGui.GetIO().Fonts; - var fontGamma = interfaceManager.FontGamma; - - var pixels8s = new byte*[ioFonts.Textures.Size]; - var pixels32s = new uint*[ioFonts.Textures.Size]; - var widths = new int[ioFonts.Textures.Size]; - var heights = new int[ioFonts.Textures.Size]; - for (var i = 0; i < pixels8s.Length; i++) - { - ioFonts.GetTexDataAsRGBA32(i, out pixels8s[i], out widths[i], out heights[i]); - pixels32s[i] = (uint*)pixels8s[i]; - } - - foreach (var (style, font) in this.fonts) - { - var fdt = this.fdts[(int)style.FamilyAndSize]; - var scale = style.SizePt / fdt.FontHeader.Size; - var fontPtr = font.NativePtr; - - Log.Verbose("[GameFontManager] AfterBuildFonts: Scaling {0} from {1}pt to {2}pt (scale: {3})", style.ToString(), fdt.FontHeader.Size, style.SizePt, scale); - - fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; - if (fontPtr->ConfigData != null) - fontPtr->ConfigData->SizePixels = fontPtr->FontSize; - fontPtr->Ascent = fdt.FontHeader.Ascent; - fontPtr->Descent = fdt.FontHeader.Descent; - fontPtr->EllipsisChar = '…'; - foreach (var fallbackCharCandidate in "〓?!") - { - var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); - if ((IntPtr)glyph.NativePtr != IntPtr.Zero) - { - var ptr = font.NativePtr; - ptr->FallbackChar = fallbackCharCandidate; - ptr->FallbackGlyph = glyph.NativePtr; - ptr->FallbackHotData = (ImFontGlyphHotData*)ptr->IndexedHotData.Address(fallbackCharCandidate); - break; - } - } - - // I have no idea what's causing NPE, so just to be safe - try - { - if (font.NativePtr != null && font.NativePtr->ConfigData != null) - { - var nameBytes = Encoding.UTF8.GetBytes(style.ToString() + "\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); - } - } - catch (NullReferenceException) - { - // do nothing - } - - foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style]) - { - var rc = (ImFontAtlasCustomRectReal*)ioFonts.GetCustomRectByIndex(rectId).NativePtr; - var pixels8 = pixels8s[rc->TextureIndex]; - var pixels32 = pixels32s[rc->TextureIndex]; - var width = widths[rc->TextureIndex]; - var height = heights[rc->TextureIndex]; - var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; - var sourceBufferDelta = glyph.TextureChannelByteIndex; - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - if (widthAdjustment == 0) - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; - pixels32[((rc->Y + y) * width) + rc->X + x] = (uint)(a << 24) | 0xFFFFFFu; - } - } - } - else - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) - pixels32[((rc->Y + y) * width) + rc->X + x] = 0xFFFFFFu; - } - - for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++) - { - var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); - for (var y = 0; y < glyph.BoundingHeight; y++) - { - float xDelta = xbold; - if (style.BaseSkewStrength > 0) - xDelta += style.BaseSkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; - else if (style.BaseSkewStrength < 0) - xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; - var xDeltaInt = (int)Math.Floor(xDelta); - var xness = xDelta - xDeltaInt; - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x; - var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; - var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))]; - var n = (a1 * xness) + (a2 * (1 - xness)); - var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; - pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n)); - } - } - } - } - - if (Math.Abs(fontGamma - 1.4f) >= 0.001) - { - // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) - for (int y = rc->Y, y_ = rc->Y + rc->Height; y < y_; y++) - { - for (int x = rc->X, x_ = rc->X + rc->Width; x < x_; x++) - { - var i = (((y * width) + x) * 4) + 3; - pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); - } - } - } - } - - UnscaleFont(font, 1 / scale, false); - } - } - - /// - /// Decrease font reference counter. - /// - /// Font to release. - internal void DecreaseFontRef(GameFontStyle style) - { - lock (this.syncRoot) - { - if (!this.fontUseCounter.ContainsKey(style)) - return; - - if ((this.fontUseCounter[style] -= 1) == 0) - this.fontUseCounter.Remove(style); - } - } - - private unsafe void EnsureFont(GameFontStyle style) - { - var rectIds = this.glyphRectIds[style] = new(); - - var fdt = this.fdts[(int)style.FamilyAndSize]; - if (fdt == null) - return; - - ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - fontConfig.PixelSnapH = false; - - var io = ImGui.GetIO(); - var font = io.Fonts.AddFontDefault(fontConfig); - - fontConfig.Destroy(); - - this.fonts[style] = font; - foreach (var glyph in fdt.Glyphs) - { - var c = glyph.Char; - if (c < 32 || c >= 0xFFFF) - continue; - - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - rectIds[c] = Tuple.Create( - io.Fonts.AddCustomRectFontGlyph( - font, - c, - glyph.BoundingWidth + widthAdjustment, - glyph.BoundingHeight, - glyph.AdvanceWidth, - new Vector2(0, glyph.CurrentOffsetY)), - glyph); - } - - foreach (var kernPair in fdt.Distances) - font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); - } -} diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index 946473df4..fbaf9de07 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -64,7 +64,7 @@ public struct GameFontStyle ///
public float SizePt { - get => this.SizePx * 3 / 4; + readonly get => this.SizePx * 3 / 4; set => this.SizePx = value * 4 / 3; } @@ -73,14 +73,14 @@ public struct GameFontStyle ///
public float BaseSkewStrength { - get => this.SkewStrength * this.BaseSizePx / this.SizePx; + readonly get => this.SkewStrength * this.BaseSizePx / this.SizePx; set => this.SkewStrength = value * this.SizePx / this.BaseSizePx; } /// /// Gets the font family. /// - public GameFontFamily Family => this.FamilyAndSize switch + public readonly GameFontFamily Family => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined, GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis, @@ -112,7 +112,7 @@ public struct GameFontStyle /// /// Gets the corresponding GameFontFamilyAndSize but with minimum possible font sizes. /// - public GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch + public readonly GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch { GameFontFamily.Axis => GameFontFamilyAndSize.Axis96, GameFontFamily.Jupiter => GameFontFamilyAndSize.Jupiter16, @@ -126,7 +126,7 @@ public struct GameFontStyle /// /// Gets the base font size in point unit. /// - public float BaseSizePt => this.FamilyAndSize switch + public readonly float BaseSizePt => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => 0, GameFontFamilyAndSize.Axis96 => 9.6f, @@ -158,14 +158,14 @@ public struct GameFontStyle /// /// Gets the base font size in pixel unit. /// - public float BaseSizePx => this.BaseSizePt * 4 / 3; + public readonly float BaseSizePx => this.BaseSizePt * 4 / 3; /// /// Gets or sets a value indicating whether this font is bold. /// public bool Bold { - get => this.Weight > 0f; + readonly get => this.Weight > 0f; set => this.Weight = value ? 1f : 0f; } @@ -174,8 +174,8 @@ public struct GameFontStyle ///
public bool Italic { - get => this.SkewStrength != 0; - set => this.SkewStrength = value ? this.SizePx / 7 : 0; + readonly get => this.SkewStrength != 0; + set => this.SkewStrength = value ? this.SizePx / 6 : 0; } /// @@ -233,13 +233,26 @@ public struct GameFontStyle _ => GameFontFamilyAndSize.Undefined, }; + /// + /// Creates a new scaled instance of struct. + /// + /// The scale. + /// The scaled instance. + public readonly GameFontStyle Scale(float scale) => new() + { + FamilyAndSize = GetRecommendedFamilyAndSize(this.Family, this.SizePt * scale), + SizePx = this.SizePx * scale, + Weight = this.Weight, + SkewStrength = this.SkewStrength * scale, + }; + /// /// Calculates the adjustment to width resulting fron Weight and SkewStrength. /// /// Font header. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + public readonly int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) { var widthDelta = this.Weight; switch (this.BaseSkewStrength) @@ -263,11 +276,11 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + public readonly int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// - public override string ToString() + public override readonly string ToString() { return $"GameFontStyle({this.FamilyAndSize}, {this.SizePt}pt, skew={this.SkewStrength}, weight={this.Weight})"; } diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index e030b4e50..28a9075bd 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -11,6 +11,7 @@ using System.Text.Unicode; using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using ImGuiNET; @@ -196,9 +197,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { - if (Service.Get() - .GetFdtReader(GameFontFamilyAndSize.Axis12) - ?.FindGlyph(chr) is null) + if (Service.Get() + ?.GetFdtReader(GameFontFamilyAndSize.Axis12) + .FindGlyph(chr) is null) { if (!this.EncounteredHan) { diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 95415659b..60c1f9957 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -21,6 +21,7 @@ using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -93,7 +94,8 @@ internal class DalamudInterface : IDisposable, IServiceType private DalamudInterface( Dalamud dalamud, DalamudConfiguration configuration, - InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, + FontAtlasFactory fontAtlasFactory, + InterfaceManager interfaceManager, PluginImageCache pluginImageCache, DalamudAssetManager dalamudAssetManager, Game.Framework framework, @@ -103,7 +105,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.dalamud = dalamud; this.configuration = configuration; - this.interfaceManager = interfaceManagerWithScene.Manager; + this.interfaceManager = interfaceManager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -122,10 +124,14 @@ internal class DalamudInterface : IDisposable, IServiceType clientState, configuration, dalamudAssetManager, + fontAtlasFactory, framework, gameGui, titleScreenMenu) { IsOpen = false }; - this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; + this.changelogWindow = new ChangelogWindow( + this.titleScreenMenuWindow, + fontAtlasFactory, + dalamudAssetManager) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false }; @@ -207,6 +213,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.interfaceManager.Draw -= this.OnDraw; + this.WindowSystem.Windows.OfType().AggregateToDisposable().Dispose(); this.WindowSystem.RemoveAllWindows(); this.changelogWindow.Dispose(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 48157fa86..3e004727a 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,13 +1,10 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; -using System.Text.Unicode; -using System.Threading; +using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -19,10 +16,13 @@ using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Storage.Assets; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -64,11 +64,9 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. - private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. - - private readonly HashSet glyphRequests = new(); - private readonly Dictionary loadedFontInfo = new(); + private const int NonMainThreadFontAccessWarningCheckInterval = 10000; + private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); + private static long nextNonMainThreadFontAccessWarningCheck; private readonly List deferredDisposeTextures = new(); @@ -81,28 +79,28 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); - private readonly ManualResetEvent fontBuildSignal; - private readonly SwapChainVtableResolver address; + private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; private RawDX11Scene? scene; private Hook? presentHook; private Hook? resizeBuffersHook; + private IFontAtlas? dalamudAtlas; + private IFontHandle.IInternal? defaultFontHandle; + private IFontHandle.IInternal? iconFontHandle; + private IFontHandle.IInternal? monoFontHandle; + // can't access imgui IO before first present call private bool lastWantCapture = false; - private bool isRebuildingFonts = false; private bool isOverrideGameCursor = true; + private IntPtr gameWindowHandle; [ServiceManager.ServiceConstructor] private InterfaceManager() { this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); - - this.fontBuildSignal = new ManualResetEvent(false); - - this.address = new SwapChainVtableResolver(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -117,43 +115,46 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// This event gets called each frame to facilitate ImGui drawing. /// - public event RawDX11Scene.BuildUIDelegate Draw; + public event RawDX11Scene.BuildUIDelegate? Draw; /// /// This event gets called when ResizeBuffers is called. /// - public event Action ResizeBuffers; - - /// - /// Gets or sets an action that is executed right before fonts are rebuilt. - /// - public event Action BuildFonts; + public event Action? ResizeBuffers; /// /// Gets or sets an action that is executed right after fonts are rebuilt. /// - public event Action AfterBuildFonts; + public event Action? AfterBuildFonts; /// - /// Gets the default ImGui font. + /// Gets the default ImGui font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr DefaultFont { get; private set; } + public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// - /// Gets an included FontAwesome icon font. + /// Gets an included FontAwesome icon font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr IconFont { get; private set; } + public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// - /// Gets an included monospaced font. + /// Gets an included monospaced font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr MonoFont { get; private set; } + public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. /// public ImGuiIOPtr LastImGuiIoPtr { get; set; } + /// + /// Gets the DX11 scene. + /// + public RawDX11Scene? Scene => this.scene; + /// /// Gets the D3D11 device instance. /// @@ -178,11 +179,6 @@ internal class InterfaceManager : IDisposable, IServiceType } } - /// - /// Gets or sets a value indicating whether the fonts are built and ready to use. - /// - public bool FontsReady { get; set; } = false; - /// /// Gets a value indicating whether the Dalamud interface ready to use. /// @@ -193,50 +189,57 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public bool IsDispatchingEvents { get; set; } = true; - /// - /// Gets or sets a value indicating whether to override configuration for UseAxis. - /// - public bool? UseAxisOverride { get; set; } = null; - - /// - /// Gets a value indicating whether to use AXIS fonts. - /// - public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; - - /// - /// Gets or sets the overrided font gamma value, instead of using the value from configuration. - /// - public float? FontGammaOverride { get; set; } = null; - - /// - /// Gets the font gamma value to use. - /// - public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGammaLevel)); - - /// - /// Gets a value indicating whether we're building fonts but haven't generated atlas yet. - /// - public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0); - /// /// Gets a value indicating the native handle of the game main window. /// - public IntPtr GameWindowHandle { get; private set; } + public IntPtr GameWindowHandle + { + get + { + if (this.gameWindowHandle == 0) + { + nint gwh = 0; + while ((gwh = NativeFunctions.FindWindowEx(0, gwh, "FFXIVGAME", 0)) != 0) + { + _ = User32.GetWindowThreadProcessId(gwh, out var pid); + if (pid == Environment.ProcessId && User32.IsWindowVisible(gwh)) + { + this.gameWindowHandle = gwh; + break; + } + } + } + + return this.gameWindowHandle; + } + } + + /// + /// Gets the font build task. + /// + public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; /// /// Dispose of managed and unmanaged resources. /// public void Dispose() { - this.framework.RunOnFrameworkThread(() => + if (Service.GetNullable() is { } framework) + framework.RunOnFrameworkThread(Disposer).Wait(); + else + Disposer(); + + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + this.dalamudAtlas?.Dispose(); + this.scene?.Dispose(); + return; + + void Disposer() { this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - }).Wait(); - - this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; - this.scene?.Dispose(); + } } #nullable enable @@ -376,93 +379,8 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public void RebuildFonts() { - if (this.scene == null) - { - Log.Verbose("[FONT] RebuildFonts(): scene not ready, doing nothing"); - return; - } - Log.Verbose("[FONT] RebuildFonts() called"); - - // don't invoke this multiple times per frame, in case multiple plugins call it - if (!this.isRebuildingFonts) - { - Log.Verbose("[FONT] RebuildFonts() trigger"); - this.isRebuildingFonts = true; - this.scene.OnNewRenderFrame += this.RebuildFontsInternal; - } - } - - /// - /// Wait for the rebuilding fonts to complete. - /// - public void WaitForFontRebuild() - { - this.fontBuildSignal.WaitOne(); - } - - /// - /// Requests a default font of specified size to exist. - /// - /// Font size in pixels. - /// Ranges of glyphs. - /// Requets handle. - public SpecialGlyphRequest NewFontSizeRef(float size, List> ranges) - { - var allContained = false; - var fonts = ImGui.GetIO().Fonts.Fonts; - ImFontPtr foundFont = null; - unsafe - { - for (int i = 0, i_ = fonts.Size; i < i_; i++) - { - if (!this.glyphRequests.Any(x => x.FontInternal.NativePtr == fonts[i].NativePtr)) - continue; - - allContained = true; - foreach (var range in ranges) - { - if (!allContained) - break; - - for (var j = range.Item1; j <= range.Item2 && allContained; j++) - allContained &= fonts[i].FindGlyphNoFallback(j).NativePtr != null; - } - - if (allContained) - foundFont = fonts[i]; - - break; - } - } - - var req = new SpecialGlyphRequest(this, size, ranges); - req.FontInternal = foundFont; - - if (!allContained) - this.RebuildFonts(); - - return req; - } - - /// - /// Requests a default font of specified size to exist. - /// - /// Font size in pixels. - /// Text to calculate glyph ranges from. - /// Requets handle. - public SpecialGlyphRequest NewFontSizeRef(float size, string text) - { - List> ranges = new(); - foreach (var c in new SortedSet(text.ToHashSet())) - { - if (ranges.Any() && ranges[^1].Item2 + 1 == c) - ranges[^1] = Tuple.Create(ranges[^1].Item1, c); - else - ranges.Add(Tuple.Create(c, c)); - } - - return this.NewFontSizeRef(size, ranges); + this.dalamudAtlas?.BuildFontsAsync(); } /// @@ -486,11 +404,11 @@ internal class InterfaceManager : IDisposable, IServiceType try { var dxgiDev = this.Device.QueryInterfaceOrNull(); - var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); + var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); if (dxgiAdapter == null) return null; - var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, SharpDX.DXGI.MemorySegmentGroup.Local); + var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, MemorySegmentGroup.Local); return (memInfo.CurrentUsage, memInfo.CurrentReservation); } catch @@ -516,20 +434,65 @@ internal class InterfaceManager : IDisposable, IServiceType /// Value. internal void SetImmersiveMode(bool enabled) { - if (this.GameWindowHandle == nint.Zero) - return; - - int value = enabled ? 1 : 0; - var hr = NativeFunctions.DwmSetWindowAttribute( - this.GameWindowHandle, - NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - ref value, - sizeof(int)); + if (this.GameWindowHandle == 0) + throw new InvalidOperationException("Game window is not yet ready."); + var value = enabled ? 1 : 0; + ((Result)NativeFunctions.DwmSetWindowAttribute( + this.GameWindowHandle, + NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + ref value, + sizeof(int))).CheckError(); } - private static void ShowFontError(string path) + private static InterfaceManager WhenFontsReady() { - Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error"); + var im = Service.GetNullable(); + if (im?.dalamudAtlas is not { } atlas) + throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call."); + + if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) + { + nextNonMainThreadFontAccessWarningCheck = + Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; + var stack = new StackTrace(); + if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) + { + if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) + { + NonMainThreadFontAccessWarning.Add(plugin, new()); + Log.Warning( + "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", + plugin.Name, + stack); + } + } + else + { + // Dalamud internal should be made safe right now + throw new InvalidOperationException("Attempted to access fonts outside the main thread."); + } + } + + if (!atlas.HasBuiltAtlas) + atlas.BuildTask.GetAwaiter().GetResult(); + return im; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RenderImGui(RawDX11Scene scene) + { + var conf = Service.Get(); + + // Process information needed by ImGuiHelpers each frame. + ImGuiHelpers.NewFrame(); + + // Enable viewports if there are no issues. + if (conf.IsDisableViewport || scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; + else + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + + scene.Render(); } private void InitScene(IntPtr swapChain) @@ -546,7 +509,7 @@ internal class InterfaceManager : IDisposable, IServiceType Service.ProvideException(ex); Log.Error(ex, "Could not load ImGui dependencies."); - var res = PInvoke.User32.MessageBox( + var res = User32.MessageBox( IntPtr.Zero, "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?", "Dalamud Error", @@ -578,7 +541,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (iniFileInfo.Length > 1200000) { Log.Warning("dalamudUI.ini was over 1mb, deleting"); - iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); + iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName!, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); iniFileInfo.Delete(); } } @@ -623,8 +586,6 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - this.SetupFonts(); - if (!configuration.IsDocking) { ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable; @@ -675,26 +636,34 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { + Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); + Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); + if (this.scene != null && swapChain != this.scene.SwapChain.NativePointer) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); if (this.scene == null) this.InitScene(swapChain); + Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); + + if (!this.dalamudAtlas!.HasBuiltAtlas) + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + if (this.address.IsReshade) { - var pRes = this.presentHook.Original(swapChain, syncInterval, presentFlags); + var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); - this.RenderImGui(); + RenderImGui(this.scene!); this.DisposeTextures(); return pRes; } - this.RenderImGui(); + RenderImGui(this.scene!); this.DisposeTextures(); - return this.presentHook.Original(swapChain, syncInterval, presentFlags); + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } private void DisposeTextures() @@ -711,471 +680,73 @@ internal class InterfaceManager : IDisposable, IServiceType } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RenderImGui() + [ServiceManager.CallWhenServicesReady( + "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] + private void ContinueConstruction( + TargetSigScanner sigScanner, + Framework framework, + FontAtlasFactory fontAtlasFactory) { - // Process information needed by ImGuiHelpers each frame. - ImGuiHelpers.NewFrame(); - - // Check if we can still enable viewports without any issues. - this.CheckViewportState(); - - this.scene.Render(); - } - - private void CheckViewportState() - { - var configuration = Service.Get(); - - if (configuration.IsDisableViewport || this.scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + this.dalamudAtlas = fontAtlasFactory + .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); + using (this.dalamudAtlas.SuppressAutoRebuild()) { - ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; - return; + this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); + this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddFontAwesomeIconFont( + new() + { + SizePx = DefaultFontSizePx, + GlyphMinAdvanceX = DefaultFontSizePx, + GlyphMaxAdvanceX = DefaultFontSizePx, + }))); + this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddDalamudAssetFont( + DalamudAsset.InconsolataRegular, + new() { SizePx = DefaultFontSizePx }))); + this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( + tk => + { + // Note: the first call of this function is done outside the main thread; this is expected. + // Do not use DefaultFont, IconFont, and MonoFont. + // Use font handles directly. + + // Fill missing glyphs in MonoFont from DefaultFont + tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); + + // Broadcast to auto-rebuilding instances + this.AfterBuildFonts?.Invoke(); + }); } - ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; - } + // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. + _ = this.dalamudAtlas.BuildFontsAsync(false); - /// - /// Loads font for use in ImGui text functions. - /// - private unsafe void SetupFonts() - { - using var setupFontsTimings = Timings.Start("IM SetupFonts"); - - var gameFontManager = Service.Get(); - var dalamud = Service.Get(); - var io = ImGui.GetIO(); - var ioFonts = io.Fonts; - - var fontGamma = this.FontGamma; - - this.fontBuildSignal.Reset(); - ioFonts.Clear(); - ioFonts.TexDesiredWidth = 4096; - - Log.Verbose("[FONT] SetupFonts - 1"); - - foreach (var v in this.loadedFontInfo) - v.Value.Dispose(); - - this.loadedFontInfo.Clear(); - - Log.Verbose("[FONT] SetupFonts - 2"); - - ImFontConfigPtr fontConfig = null; - List garbageList = new(); + this.address.Setup(sigScanner); try { - var dummyRangeHandle = GCHandle.Alloc(new ushort[] { '0', '0', 0 }, GCHandleType.Pinned); - garbageList.Add(dummyRangeHandle); - - fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - - var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Regular.otf"); - if (!File.Exists(fontPathJp)) - fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); - if (!File.Exists(fontPathJp)) - ShowFontError(fontPathJp); - Log.Verbose("[FONT] fontPathJp = {0}", fontPathJp); - - var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); - if (!File.Exists(fontPathKr)) - fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); - if (!File.Exists(fontPathKr)) - fontPathKr = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "malgun.ttf"); - if (!File.Exists(fontPathKr)) - fontPathKr = null; - Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); - - var fontPathChs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msyh.ttc"); - if (!File.Exists(fontPathChs)) - fontPathChs = null; - Log.Verbose("[FONT] fontPathChs = {0}", fontPathChs); - - var fontPathCht = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msjh.ttc"); - if (!File.Exists(fontPathCht)) - fontPathCht = null; - Log.Verbose("[FONT] fontPathChs = {0}", fontPathCht); - - // Default font - Log.Verbose("[FONT] SetupFonts - Default font"); - var fontInfo = new TargetFontModification( - "Default", - this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, - this.UseAxis ? DefaultFontSizePx : DefaultFontSizePx + 1, - io.FontGlobalScale); - Log.Verbose("[FONT] SetupFonts - Default corresponding AXIS size: {0}pt ({1}px)", fontInfo.SourceAxis.Style.BaseSizePt, fontInfo.SourceAxis.Style.BaseSizePx); - fontConfig.SizePixels = fontInfo.TargetSizePx * io.FontGlobalScale; - if (this.UseAxis) - { - fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = false; - DefaultFont = ioFonts.AddFontDefault(fontConfig); - this.loadedFontInfo[DefaultFont] = fontInfo; - } - else - { - var rangeHandle = gameFontManager.ToGlyphRanges(GameFontFamilyAndSize.Axis12); - garbageList.Add(rangeHandle); - - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontConfig.SizePixels, fontConfig); - this.loadedFontInfo[DefaultFont] = fontInfo; - } - - if (fontPathKr != null - && (Service.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul)) - { - fontConfig.MergeMode = true; - fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathKr, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - - if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") - { - fontConfig.MergeMode = true; - var rangeHandle = GCHandle.Alloc(new ushort[] - { - (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), - (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), - 0, - }, GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathCht, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" - || this.dalamudIme.EncounteredHan)) - { - fontConfig.MergeMode = true; - var rangeHandle = GCHandle.Alloc(new ushort[] - { - (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), - (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), - 0, - }, GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathChs, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - - // FontAwesome icon font - Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); - { - var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesomeFreeSolid.otf"); - if (!File.Exists(fontPathIcon)) - ShowFontError(fontPathIcon); - - var iconRangeHandle = GCHandle.Alloc(new ushort[] { 0xE000, 0xF8FF, 0, }, GCHandleType.Pinned); - garbageList.Add(iconRangeHandle); - - fontConfig.GlyphRanges = iconRangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, DefaultFontSizePx * io.FontGlobalScale, fontConfig); - this.loadedFontInfo[IconFont] = new("Icon", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); - } - - // Monospace font - Log.Verbose("[FONT] SetupFonts - Monospace font"); - { - var fontPathMono = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf"); - if (!File.Exists(fontPathMono)) - ShowFontError(fontPathMono); - - fontConfig.GlyphRanges = IntPtr.Zero; - fontConfig.PixelSnapH = true; - MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, DefaultFontSizePx * io.FontGlobalScale, fontConfig); - this.loadedFontInfo[MonoFont] = new("Mono", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); - } - - // Default font but in requested size for requested glyphs - Log.Verbose("[FONT] SetupFonts - Default font but in requested size for requested glyphs"); - { - Dictionary> extraFontRequests = new(); - foreach (var extraFontRequest in this.glyphRequests) - { - if (!extraFontRequests.ContainsKey(extraFontRequest.Size)) - extraFontRequests[extraFontRequest.Size] = new(); - extraFontRequests[extraFontRequest.Size].Add(extraFontRequest); - } - - foreach (var (fontSize, requests) in extraFontRequests) - { - List<(ushort, ushort)> codepointRanges = new(4 + requests.Sum(x => x.CodepointRanges.Count)) - { - new(Fallback1Codepoint, Fallback1Codepoint), - new(Fallback2Codepoint, Fallback2Codepoint), - // ImGui default ellipsis characters - new(0x2026, 0x2026), - new(0x0085, 0x0085), - }; - - foreach (var request in requests) - codepointRanges.AddRange(request.CodepointRanges.Select(x => (From: x.Item1, To: x.Item2))); - - codepointRanges.Sort(); - List flattenedRanges = new(); - foreach (var range in codepointRanges) - { - if (flattenedRanges.Any() && flattenedRanges[^1] >= range.Item1 - 1) - { - flattenedRanges[^1] = Math.Max(flattenedRanges[^1], range.Item2); - } - else - { - flattenedRanges.Add(range.Item1); - flattenedRanges.Add(range.Item2); - } - } - - flattenedRanges.Add(0); - - fontInfo = new( - $"Requested({fontSize}px)", - this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, - fontSize, - io.FontGlobalScale); - if (this.UseAxis) - { - fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.SizePixels = fontInfo.SourceAxis.Style.BaseSizePx; - fontConfig.PixelSnapH = false; - - var sizedFont = ioFonts.AddFontDefault(fontConfig); - this.loadedFontInfo[sizedFont] = fontInfo; - foreach (var request in requests) - request.FontInternal = sizedFont; - } - else - { - var rangeHandle = GCHandle.Alloc(flattenedRanges.ToArray(), GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.PixelSnapH = true; - - var sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontSize * io.FontGlobalScale, fontConfig, rangeHandle.AddrOfPinnedObject()); - this.loadedFontInfo[sizedFont] = fontInfo; - foreach (var request in requests) - request.FontInternal = sizedFont; - } - } - } - - gameFontManager.BuildFonts(); - - var customFontFirstConfigIndex = ioFonts.ConfigData.Size; - - Log.Verbose("[FONT] Invoke OnBuildFonts"); - this.BuildFonts?.InvokeSafely(); - Log.Verbose("[FONT] OnBuildFonts OK!"); - - for (int i = customFontFirstConfigIndex, i_ = ioFonts.ConfigData.Size; i < i_; i++) - { - var config = ioFonts.ConfigData[i]; - if (gameFontManager.OwnsFont(config.DstFont)) - continue; - - config.OversampleH = 1; - config.OversampleV = 1; - - var name = Encoding.UTF8.GetString((byte*)config.Name.Data, config.Name.Count).TrimEnd('\0'); - if (name.IsNullOrEmpty()) - name = $"{config.SizePixels}px"; - - // ImFont information is reflected only if corresponding ImFontConfig has MergeMode not set. - if (config.MergeMode) - { - if (!this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) - { - Log.Warning("MergeMode specified for {0} but not found in loadedFontInfo. Skipping.", name); - continue; - } - } - else - { - if (this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) - { - Log.Warning("MergeMode not specified for {0} but found in loadedFontInfo. Skipping.", name); - continue; - } - - // While the font will be loaded in the scaled size after FontScale is applied, the font will be treated as having the requested size when used from plugins. - this.loadedFontInfo[config.DstFont.NativePtr] = new($"PlReq({name})", config.SizePixels); - } - - config.SizePixels = config.SizePixels * io.FontGlobalScale; - } - - for (int i = 0, i_ = ioFonts.ConfigData.Size; i < i_; i++) - { - var config = ioFonts.ConfigData[i]; - config.RasterizerGamma *= fontGamma; - } - - Log.Verbose("[FONT] ImGui.IO.Build will be called."); - ioFonts.Build(); - gameFontManager.AfterIoFontsBuild(); - this.ClearStacks(); - Log.Verbose("[FONT] ImGui.IO.Build OK!"); - - gameFontManager.AfterBuildFonts(); - - foreach (var (font, mod) in this.loadedFontInfo) - { - // I have no idea what's causing NPE, so just to be safe - try - { - if (font.NativePtr != null && font.NativePtr->ConfigData != null) - { - var nameBytes = Encoding.UTF8.GetBytes($"{mod.Name}\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); - } - } - catch (NullReferenceException) - { - // do nothing - } - - Log.Verbose("[FONT] {0}: Unscale with scale value of {1}", mod.Name, mod.Scale); - GameFontManager.UnscaleFont(font, mod.Scale, false); - - if (mod.Axis == TargetFontModification.AxisMode.Overwrite) - { - Log.Verbose("[FONT] {0}: Overwrite from AXIS of size {1}px (was {2}px)", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); - GameFontManager.UnscaleFont(font, font.FontSize / mod.SourceAxis.ImFont.FontSize, false); - var ascentDiff = mod.SourceAxis.ImFont.Ascent - font.Ascent; - font.Ascent += ascentDiff; - font.Descent = ascentDiff; - font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar; - font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar; - ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); - } - else if (mod.Axis == TargetFontModification.AxisMode.GameGlyphsOnly) - { - Log.Verbose("[FONT] {0}: Overwrite game specific glyphs from AXIS of size {1}px", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); - if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) - mod.SourceAxis.ImFont.FontSize -= 1; - ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); - if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) - mod.SourceAxis.ImFont.FontSize += 1; - } - - Log.Verbose("[FONT] {0}: Resize from {1}px to {2}px", mod.Name, font.FontSize, mod.TargetSizePx); - GameFontManager.UnscaleFont(font, font.FontSize / mod.TargetSizePx, false); - } - - // Fill missing glyphs in MonoFont from DefaultFont - ImGuiHelpers.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); - - for (int i = 0, i_ = ioFonts.Fonts.Size; i < i_; i++) - { - var font = ioFonts.Fonts[i]; - if (font.Glyphs.Size == 0) - { - Log.Warning("[FONT] Font has no glyph: {0}", font.GetDebugName()); - continue; - } - - if (font.FindGlyphNoFallback(Fallback1Codepoint).NativePtr != null) - font.FallbackChar = Fallback1Codepoint; - - font.BuildLookupTableNonstandard(); - } - - Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); - this.AfterBuildFonts?.InvokeSafely(); - Log.Verbose("[FONT] OnAfterBuildFonts OK!"); - - if (ioFonts.Fonts[0].NativePtr != DefaultFont.NativePtr) - Log.Warning("[FONT] First font is not DefaultFont"); - - Log.Verbose("[FONT] Fonts built!"); - - this.fontBuildSignal.Set(); - - this.FontsReady = true; + if (Service.Get().WindowIsImmersive) + this.SetImmersiveMode(true); } - finally + catch (Exception ex) { - if (fontConfig.NativePtr != null) - fontConfig.Destroy(); - - foreach (var garbage in garbageList) - garbage.Free(); + Log.Error(ex, "Could not enable immersive mode"); } - } - [ServiceManager.CallWhenServicesReady( - "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] - private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration) - { - this.address.Setup(sigScanner); - this.framework.RunOnFrameworkThread(() => - { - while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) - { - _ = User32.GetWindowThreadProcessId(this.GameWindowHandle, out var pid); + this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); + this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); - if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle)) - break; - } + Log.Verbose("===== S W A P C H A I N ====="); + Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); + Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - try - { - if (configuration.WindowIsImmersive) - this.SetImmersiveMode(true); - } - catch (Exception ex) - { - Log.Error(ex, "Could not enable immersive mode"); - } - - this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); - - Log.Verbose("===== S W A P C H A I N ====="); - Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); - Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - - this.setCursorHook.Enable(); - this.presentHook.Enable(); - this.resizeBuffersHook.Enable(); - }); - } - - // This is intended to only be called as a handler attached to scene.OnNewRenderFrame - private void RebuildFontsInternal() - { - Log.Verbose("[FONT] RebuildFontsInternal() called"); - this.SetupFonts(); - - Log.Verbose("[FONT] RebuildFontsInternal() detaching"); - this.scene!.OnNewRenderFrame -= this.RebuildFontsInternal; - - Log.Verbose("[FONT] Calling InvalidateFonts"); - this.scene.InvalidateFonts(); - - Log.Verbose("[FONT] Font Rebuild OK!"); - - this.isRebuildingFonts = false; + this.setCursorHook.Enable(); + this.presentHook.Enable(); + this.resizeBuffersHook.Enable(); } private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) @@ -1206,14 +777,17 @@ internal class InterfaceManager : IDisposable, IServiceType private IntPtr SetCursorDetour(IntPtr hCursor) { - if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) + if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); + return this.setCursorHook.IsDisposed + ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() + : this.setCursorHook.Original(hCursor); } private void OnNewInputFrame() { + var io = ImGui.GetIO(); var dalamudInterface = Service.GetNullable(); var gamepadState = Service.GetNullable(); var keyState = Service.GetNullable(); @@ -1221,18 +795,21 @@ internal class InterfaceManager : IDisposable, IServiceType if (dalamudInterface == null || gamepadState == null || keyState == null) return; + // Prevent setting the footgun from ImGui Demo; the Space key isn't removing the flag at the moment. + io.ConfigFlags &= ~ImGuiConfigFlags.NoMouse; + // fix for keys in game getting stuck, if you were holding a game key (like run) // and then clicked on an imgui textbox - imgui would swallow the keyup event, // so the game would think the key remained pressed continuously until you left // imgui and pressed and released the key again - if (ImGui.GetIO().WantTextInput) + if (io.WantTextInput) { keyState.ClearAll(); } // TODO: mouse state? - var gamepadEnabled = (ImGui.GetIO().BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; + var gamepadEnabled = (io.BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; // NOTE (Chiv) Activate ImGui navigation via L1+L3 press // (mimicking how mouse navigation is activated via L1+R3 press in game). @@ -1240,12 +817,12 @@ internal class InterfaceManager : IDisposable, IServiceType && gamepadState.Raw(GamepadButtons.L1) > 0 && gamepadState.Pressed(GamepadButtons.L3) > 0) { - ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; + io.ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; gamepadState.NavEnableGamepad ^= true; dalamudInterface.ToggleGamepadModeNotifierWindow(); } - if (gamepadEnabled && (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) + if (gamepadEnabled && (io.ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) { var northButton = gamepadState.Raw(GamepadButtons.North) != 0; var eastButton = gamepadState.Raw(GamepadButtons.East) != 0; @@ -1264,7 +841,6 @@ internal class InterfaceManager : IDisposable, IServiceType var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0; var r2Button = gamepadState.Raw(GamepadButtons.R2) != 0; - var io = ImGui.GetIO(); io.AddKeyEvent(ImGuiKey.GamepadFaceUp, northButton); io.AddKeyEvent(ImGuiKey.GamepadFaceRight, eastButton); io.AddKeyEvent(ImGuiKey.GamepadFaceDown, southButton); @@ -1312,7 +888,10 @@ internal class InterfaceManager : IDisposable, IServiceType var snap = ImGuiManagedAsserts.GetSnapshot(); if (this.IsDispatchingEvents) - this.Draw?.Invoke(); + { + using (this.defaultFontHandle?.Push()) + this.Draw?.Invoke(); + } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); @@ -1339,123 +918,4 @@ internal class InterfaceManager : IDisposable, IServiceType /// public InterfaceManager Manager { get; init; } } - - /// - /// Represents a glyph request. - /// - public class SpecialGlyphRequest : IDisposable - { - /// - /// Initializes a new instance of the class. - /// - /// InterfaceManager to associate. - /// Font size in pixels. - /// Codepoint ranges. - internal SpecialGlyphRequest(InterfaceManager manager, float size, List> ranges) - { - this.Manager = manager; - this.Size = size; - this.CodepointRanges = ranges; - this.Manager.glyphRequests.Add(this); - } - - /// - /// Gets the font of specified size, or DefaultFont if it's not ready yet. - /// - public ImFontPtr Font - { - get - { - unsafe - { - return this.FontInternal.NativePtr == null ? DefaultFont : this.FontInternal; - } - } - } - - /// - /// Gets or sets the associated ImFont. - /// - internal ImFontPtr FontInternal { get; set; } - - /// - /// Gets associated InterfaceManager. - /// - internal InterfaceManager Manager { get; init; } - - /// - /// Gets font size. - /// - internal float Size { get; init; } - - /// - /// Gets codepoint ranges. - /// - internal List> CodepointRanges { get; init; } - - /// - public void Dispose() - { - this.Manager.glyphRequests.Remove(this); - } - } - - private unsafe class TargetFontModification : IDisposable - { - /// - /// Initializes a new instance of the class. - /// Constructs new target font modification information, assuming that AXIS fonts will not be applied. - /// - /// Name of the font to write to ImGui font information. - /// Target font size in pixels, which will not be considered for further scaling. - internal TargetFontModification(string name, float sizePx) - { - this.Name = name; - this.Axis = AxisMode.Suppress; - this.TargetSizePx = sizePx; - this.Scale = 1; - this.SourceAxis = null; - } - - /// - /// Initializes a new instance of the class. - /// Constructs new target font modification information. - /// - /// Name of the font to write to ImGui font information. - /// Whether and how to use AXIS fonts. - /// Target font size in pixels, which will not be considered for further scaling. - /// Font scale to be referred for loading AXIS font of appropriate size. - internal TargetFontModification(string name, AxisMode axis, float sizePx, float globalFontScale) - { - this.Name = name; - this.Axis = axis; - this.TargetSizePx = sizePx; - this.Scale = globalFontScale; - this.SourceAxis = Service.Get().NewFontRef(new(GameFontFamily.Axis, this.TargetSizePx * this.Scale)); - } - - internal enum AxisMode - { - Suppress, - GameGlyphsOnly, - Overwrite, - } - - internal string Name { get; private init; } - - internal AxisMode Axis { get; private init; } - - internal float TargetSizePx { get; private init; } - - internal float Scale { get; private init; } - - internal GameFontHandle? SourceAxis { get; private init; } - - internal bool SourceAxisAvailable => this.SourceAxis != null && this.SourceAxis.ImFont.NativePtr != null; - - public void Dispose() - { - this.SourceAxis?.Dispose(); - } - } } diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index b9e7ab686..ae59db36a 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Linq; using System.Numerics; @@ -7,6 +6,8 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -31,8 +32,14 @@ internal sealed class ChangelogWindow : Window, IDisposable • Plugins can now add tooltips and interaction to the server info bar • The Dalamud/plugin installer UI has been refreshed "; - + private readonly TitleScreenMenuWindow tsmWindow; + + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private readonly Lazy bannerFont; + private readonly Lazy apiBumpExplainerTexture; + private readonly Lazy logoTexture; private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f)) { @@ -46,27 +53,36 @@ internal sealed class ChangelogWindow : Window, IDisposable Point2 = Vector2.One, }; - private IDalamudTextureWrap? apiBumpExplainerTexture; - private IDalamudTextureWrap? logoTexture; - private GameFontHandle? bannerFont; - private State state = State.WindowFadeIn; private bool needFadeRestart = false; - + /// /// Initializes a new instance of the class. /// /// TSM window. - public ChangelogWindow(TitleScreenMenuWindow tsmWindow) + /// An instance of . + /// An instance of . + public ChangelogWindow( + TitleScreenMenuWindow tsmWindow, + FontAtlasFactory fontAtlasFactory, + DalamudAssetManager assets) : base("What's new in Dalamud?##ChangelogWindow", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse, true) { this.tsmWindow = tsmWindow; this.Namespace = "DalamudChangelogWindow"; + this.privateAtlas = this.scopedFinalizer.Add( + fontAtlasFactory.CreateFontAtlas(this.Namespace, FontAtlasAutoRebuildMode.Async)); + this.bannerFont = new( + () => this.scopedFinalizer.Add( + this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.MiedingerMid18)))); + + this.apiBumpExplainerTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.ChangelogApiBumpIcon)); + this.logoTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.Logo)); // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); + _ = this.bannerFont.Value; } private enum State @@ -97,20 +113,12 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - this.MakeFont(Service.Get()); + _ = this.bannerFont; this.state = State.WindowFadeIn; this.windowFade.Reset(); this.bodyFade.Reset(); this.needFadeRestart = true; - - if (this.apiBumpExplainerTexture == null) - { - var dalamud = Service.Get(); - var tm = Service.Get(); - this.apiBumpExplainerTexture = tm.GetTextureFromFile(new FileInfo(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "changelogApiBump.png"))) - ?? throw new Exception("Could not load api bump explainer."); - } base.OnOpen(); } @@ -186,10 +194,7 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.SetCursorPos(new Vector2(logoContainerSize.X / 2 - logoSize.X / 2, logoContainerSize.Y / 2 - logoSize.Y / 2)); using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f))) - { - this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); - ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); - } + ImGui.Image(this.logoTexture.Value.ImGuiHandle, logoSize); } ImGui.SameLine(); @@ -205,7 +210,7 @@ internal sealed class ChangelogWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f))) { - using var font = ImRaii.PushFont(this.bannerFont!.ImFont); + using var font = this.bannerFont.Value.Push(); switch (this.state) { @@ -275,9 +280,11 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.TextWrapped("If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available."); ImGuiHelpers.ScaledDummy(15); - - ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width); - ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size); + + ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture.Value.Width); + ImGui.Image( + this.apiBumpExplainerTexture.Value.ImGuiHandle, + this.apiBumpExplainerTexture.Value.Size); DrawNextButton(State.Links); break; @@ -377,7 +384,4 @@ internal sealed class ChangelogWindow : Window, IDisposable public void Dispose() { } - - private void MakeFont(GameFontManager gfm) => - this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); } diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 20c3d6d01..951d3d91c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -6,6 +6,8 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; +using Dalamud.Utility; + using ImGuiNET; using Serilog; @@ -14,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class responsible for drawing the data/debug window. /// -internal class DataWindow : Window +internal class DataWindow : Window, IDisposable { private readonly IDataWindowWidget[] modules = { @@ -34,6 +36,7 @@ internal class DataWindow : Window new FlyTextWidget(), new FontAwesomeTestWidget(), new GameInventoryTestWidget(), + new GamePrebakedFontsTestWidget(), new GamepadWidget(), new GaugeWidget(), new HookWidget(), @@ -76,6 +79,9 @@ internal class DataWindow : Window this.Load(); } + /// + public void Dispose() => this.modules.OfType().AggregateToDisposable().Dispose(); + /// public override void OnOpen() { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs new file mode 100644 index 000000000..dba293e8b --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -0,0 +1,213 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget for testing game prebaked fonts. +/// +internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable +{ + private ImVectorWrapper testStringBuffer; + private IFontAtlas? privateAtlas; + private IReadOnlyDictionary Handle)[]>? fontHandles; + private bool useGlobalScale; + private bool useWordWrap; + private bool useItalic; + private bool useBold; + private bool useMinimumBuild; + + /// + public string[]? CommandShortcuts { get; init; } + + /// + public string DisplayName { get; init; } = "Game Prebaked Fonts"; + + /// + public bool Ready { get; set; } + + /// + public void Load() => this.Ready = true; + + /// + public unsafe void Draw() + { + ImGui.AlignTextToFramePadding(); + fixed (byte* labelPtr = "Global Scale"u8) + { + var v = (byte)(this.useGlobalScale ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useGlobalScale = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Word Wrap"u8) + { + var v = (byte)(this.useWordWrap ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + this.useWordWrap = v != 0; + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Italic"u8) + { + var v = (byte)(this.useItalic ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useItalic = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Bold"u8) + { + var v = (byte)(this.useBold ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useBold = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Minimum Range"u8) + { + var v = (byte)(this.useMinimumBuild ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useMinimumBuild = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed) + { + this.testStringBuffer.Dispose(); + this.testStringBuffer = ImVectorWrapper.CreateFromSpan( + "(Game)-[Font] {Test}. 0123456789!! <氣気气きキ기>。"u8, + minCapacity: 1024); + } + + fixed (byte* labelPtr = "Test Input"u8) + { + if (ImGuiNative.igInputTextMultiline( + labelPtr, + this.testStringBuffer.Data, + (uint)this.testStringBuffer.Capacity, + new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale), + 0, + null, + null) != 0) + { + var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); + if (len + 4 >= this.testStringBuffer.Capacity) + this.testStringBuffer.EnsureCapacityExponential(len + 4); + if (len < this.testStringBuffer.Capacity) + { + this.testStringBuffer.LengthUnsafe = len; + this.testStringBuffer.StorageSpan[len] = default; + } + + if (this.useMinimumBuild) + _ = this.privateAtlas?.BuildFontsAsync(); + } + } + + this.privateAtlas ??= + Service.Get().CreateFontAtlas( + nameof(GamePrebakedFontsTestWidget), + FontAtlasAutoRebuildMode.Async, + this.useGlobalScale); + this.fontHandles ??= + Enum.GetValues() + .Where(x => x.GetAttribute() is not null) + .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) + .GroupBy(x => x.Family) + .ToImmutableDictionary( + x => x.Key, + x => x.Select( + y => (y, new Lazy( + () => this.useMinimumBuild + ? this.privateAtlas.NewDelegateFontHandle( + e => + e.OnPreBuild( + tk => tk.AddGameGlyphs( + y, + Encoding.UTF8.GetString( + this.testStringBuffer.DataSpan).ToGlyphRange(), + default))) + : this.privateAtlas.NewGameFontHandle(y)))) + .ToArray()); + + var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); + foreach (var (family, items) in this.fontHandles) + { + if (!ImGui.CollapsingHeader($"{family} Family")) + continue; + + foreach (var (gfs, handle) in items) + { + ImGui.TextUnformatted($"{gfs.SizePt}pt"); + ImGui.SameLine(offsetX); + ImGuiNative.igPushTextWrapPos(this.useWordWrap ? 0f : -1f); + try + { + if (handle.Value.LoadException is { } exc) + { + ImGui.TextUnformatted(exc.ToString()); + } + else if (!handle.Value.Available) + { + fixed (byte* labelPtr = "Loading..."u8) + ImGuiNative.igTextUnformatted(labelPtr, labelPtr + 8 + ((Environment.TickCount / 200) % 3)); + } + else + { + if (!this.useGlobalScale) + ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); + using var pushPop = handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + } + } + finally + { + ImGuiNative.igPopTextWrapPos(); + ImGuiNative.igSetWindowFontScale(1); + } + } + } + } + + /// + public void Dispose() + { + this.ClearAtlas(); + this.testStringBuffer.Dispose(); + } + + private void ClearAtlas() + { + this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value)) + .AggregateToDisposable().Dispose(); + this.fontHandles = null; + this.privateAtlas?.Dispose(); + this.privateAtlas = null; + } +} diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 7d4489f8d..027e1a571 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -5,10 +5,10 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.Settings.Tabs; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Plugin.Internal; using Dalamud.Utility; using ImGuiNET; @@ -19,14 +19,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings; ///
internal class SettingsWindow : Window { - private readonly SettingsTab[] tabs = - { - new SettingsTabGeneral(), - new SettingsTabLook(), - new SettingsTabDtr(), - new SettingsTabExperimental(), - new SettingsTabAbout(), - }; + private SettingsTab[]? tabs; private string searchInput = string.Empty; @@ -49,6 +42,15 @@ internal class SettingsWindow : Window /// public override void OnOpen() { + this.tabs ??= new SettingsTab[] + { + new SettingsTabGeneral(), + new SettingsTabLook(), + new SettingsTabDtr(), + new SettingsTabExperimental(), + new SettingsTabAbout(), + }; + foreach (var settingsTab in this.tabs) { settingsTab.Load(); @@ -64,15 +66,12 @@ internal class SettingsWindow : Window { var configuration = Service.Get(); var interfaceManager = Service.Get(); + var fontAtlasFactory = Service.Get(); - var rebuildFont = - ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale || - interfaceManager.FontGamma != configuration.FontGammaLevel || - interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - interfaceManager.FontGammaOverride = null; - interfaceManager.UseAxisOverride = null; + fontAtlasFactory.UseAxisOverride = null; if (rebuildFont) interfaceManager.RebuildFonts(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 5b6f6b02f..8714fd666 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -1,13 +1,13 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Numerics; using CheapLoc; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; @@ -15,7 +15,6 @@ using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; @@ -173,16 +172,21 @@ Contribute at: https://github.com/goatcorp/Dalamud "; private readonly Stopwatch creditsThrottler; + private readonly IFontAtlas privateAtlas; private string creditsText; private bool resetNow = false; private IDalamudTextureWrap? logoTexture; - private GameFontHandle? thankYouFont; + private IFontHandle? thankYouFont; public SettingsTabAbout() { this.creditsThrottler = new(); + + this.privateAtlas = Service + .Get() + .CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async); } public override SettingsEntry[] Entries { get; } = { }; @@ -207,11 +211,7 @@ Contribute at: https://github.com/goatcorp/Dalamud this.creditsThrottler.Restart(); - if (this.thankYouFont == null) - { - var gfm = Service.Get(); - this.thankYouFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.TrumpGothic34)); - } + this.thankYouFont ??= this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.TrumpGothic34)); this.resetNow = true; @@ -269,14 +269,12 @@ Contribute at: https://github.com/goatcorp/Dalamud if (this.thankYouFont != null) { - ImGui.PushFont(this.thankYouFont.ImFont); + using var fontPush = this.thankYouFont.Push(); var thankYouLenX = ImGui.CalcTextSize(ThankYouText).X; ImGui.Dummy(new Vector2((windowX / 2) - (thankYouLenX / 2), 0f)); ImGui.SameLine(); ImGui.TextUnformatted(ThankYouText); - - ImGui.PopFont(); } ImGuiHelpers.ScaledDummy(0, windowSize.Y + 50f); @@ -305,9 +303,5 @@ Contribute at: https://github.com/goatcorp/Dalamud /// /// Disposes of managed and unmanaged resources. /// - public override void Dispose() - { - this.logoTexture?.Dispose(); - this.thankYouFont?.Dispose(); - } + public override void Dispose() => this.privateAtlas.Dispose(); } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 02e8ce789..5293e13c4 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,12 +1,14 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; +using System.Text; using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -28,7 +30,6 @@ public class SettingsTabLook : SettingsTab }; private float globalUiScale; - private float fontGamma; public override SettingsEntry[] Entries { get; } = { @@ -41,9 +42,8 @@ public class SettingsTabLook : SettingsTab (v, c) => c.UseAxisFontsFromGame = v, v => { - var im = Service.Get(); - im.UseAxisOverride = v; - im.RebuildFonts(); + Service.Get().UseAxisOverride = v; + Service.Get().RebuildFonts(); }), new GapSettingsEntry(5, true), @@ -145,6 +145,7 @@ public class SettingsTabLook : SettingsTab public override void Draw() { var interfaceManager = Service.Get(); + var fontBuildTask = interfaceManager.FontBuildTask; ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); @@ -164,6 +165,19 @@ public class SettingsTabLook : SettingsTab } } + if (!fontBuildTask.IsCompleted) + { + ImGui.SameLine(); + var buildingFonts = Loc.Localize("DalamudSettingsFontBuildInProgressWithEndingThreeDots", "Building fonts..."); + unsafe + { + var len = Encoding.UTF8.GetByteCount(buildingFonts); + var p = stackalloc byte[len]; + Encoding.UTF8.GetBytes(buildingFonts, new(p, len)); + ImGuiNative.igTextUnformatted(p, (p + len + ((Environment.TickCount / 200) % 3)) - 2); + } + } + var globalUiScaleInPt = 12f * this.globalUiScale; if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp)) { @@ -174,33 +188,25 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale text in all XIVLauncher UI elements - this is useful for 4K displays.")); - ImGuiHelpers.ScaledDummy(5); - - ImGui.AlignTextToFramePadding(); - ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); - ImGui.SameLine(); - if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) + if (fontBuildTask.IsFaulted || fontBuildTask.IsCanceled) { - this.fontGamma = 1.4f; - interfaceManager.FontGammaOverride = this.fontGamma; - interfaceManager.RebuildFonts(); + ImGui.TextColored( + ImGuiColors.DalamudRed, + Loc.Localize("DalamudSettingsFontBuildFaulted", "Failed to load fonts as requested.")); + if (fontBuildTask.Exception is not null + && ImGui.CollapsingHeader("##DalamudSetingsFontBuildFaultReason")) + { + foreach (var e in fontBuildTask.Exception.InnerExceptions) + ImGui.TextUnformatted(e.ToString()); + } } - if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp)) - { - interfaceManager.FontGammaOverride = this.fontGamma; - interfaceManager.RebuildFonts(); - } - - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFontGammaHint", "Changes the thickness of text.")); - base.Draw(); } public override void Load() { this.globalUiScale = Service.Get().GlobalUiScale; - this.fontGamma = Service.Get().FontGammaLevel; base.Load(); } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 42bca89ff..9c385a99c 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -7,11 +7,14 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; +using Dalamud.Utility; using ImGuiNET; @@ -27,16 +30,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly ClientState clientState; private readonly DalamudConfiguration configuration; - private readonly Framework framework; private readonly GameGui gameGui; private readonly TitleScreenMenu titleScreenMenu; + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private readonly Lazy myFontHandle; private readonly Lazy shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); private readonly Dictionary logoEasings = new(); - private readonly Dictionary specialGlyphRequests = new(); private InOutCubic? fadeOutEasing; @@ -48,6 +52,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// An instance of . /// An instance of . /// An instance of . + /// An instance of . /// An instance of . /// An instance of . /// An instance of . @@ -55,6 +60,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable ClientState clientState, DalamudConfiguration configuration, DalamudAssetManager dalamudAssetManager, + FontAtlasFactory fontAtlasFactory, Framework framework, GameGui gameGui, TitleScreenMenu titleScreenMenu) @@ -65,7 +71,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable { this.clientState = clientState; this.configuration = configuration; - this.framework = framework; this.gameGui = gameGui; this.titleScreenMenu = titleScreenMenu; @@ -77,9 +82,25 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); + this.privateAtlas = fontAtlasFactory.CreateFontAtlas(this.WindowName, FontAtlasAutoRebuildMode.Async); + this.scopedFinalizer.Add(this.privateAtlas); + + this.myFontHandle = new( + () => this.scopedFinalizer.Add( + this.privateAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + toolkit => toolkit.AddDalamudDefaultFont( + TargetFontSizePx, + titleScreenMenu.Entries.SelectMany(x => x.Name).ToGlyphRange()))))); + + titleScreenMenu.EntryListChange += this.TitleScreenMenuEntryListChange; + this.scopedFinalizer.Add(() => titleScreenMenu.EntryListChange -= this.TitleScreenMenuEntryListChange); + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); framework.Update += this.FrameworkOnUpdate; + this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate); } private enum State @@ -94,6 +115,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public bool AllowDrawing { get; set; } = true; + /// + public void Dispose() => this.scopedFinalizer.Dispose(); + /// public override void PreDraw() { @@ -109,12 +133,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable base.PostDraw(); } - /// - public void Dispose() - { - this.framework.Update -= this.FrameworkOnUpdate; - } - /// public override void Draw() { @@ -246,33 +264,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable break; } } - - var srcText = entries.Select(e => e.Name).ToHashSet(); - var keys = this.specialGlyphRequests.Keys.ToHashSet(); - keys.RemoveWhere(x => srcText.Contains(x)); - foreach (var key in keys) - { - this.specialGlyphRequests[key].Dispose(); - this.specialGlyphRequests.Remove(key); - } } private bool DrawEntry( TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) { - InterfaceManager.SpecialGlyphRequest fontHandle; - if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx) - { - fontHandle.Dispose(); - this.specialGlyphRequests.Remove(entry.Name); - fontHandle = null; - } - - if (fontHandle == null) - this.specialGlyphRequests[entry.Name] = fontHandle = Service.Get().NewFontSizeRef(TargetFontSizePx, entry.Name); - - ImGui.PushFont(fontHandle.Font); - ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size); + using var fontScopeDispose = this.myFontHandle.Value.Push(); var scale = ImGui.GetIO().FontGlobalScale; @@ -383,8 +380,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable initialCursor.Y += entry.Texture.Height * scale; ImGui.SetCursorPos(initialCursor); - ImGui.PopFont(); - return isHover; } @@ -401,4 +396,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero) this.IsOpen = false; } + + private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs new file mode 100644 index 000000000..50e591390 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// How to rebuild . +/// +public enum FontAtlasAutoRebuildMode +{ + /// + /// Do not rebuild. + /// + Disable, + + /// + /// Rebuild on new frame. + /// + OnNewFrame, + + /// + /// Rebuild asynchronously. + /// + Async, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs new file mode 100644 index 000000000..345ab729d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs @@ -0,0 +1,38 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Build step for . +/// +public enum FontAtlasBuildStep +{ + /// + /// An invalid value. This should never be passed through event callbacks. + /// + Invalid, + + /// + /// Called before calling .
+ /// Expect to be passed. + ///
+ PreBuild, + + /// + /// Called after calling .
+ /// Expect to be passed.
+ ///
+ /// This callback is not guaranteed to happen after , + /// but it will never happen on its own. + ///
+ PostBuild, + + /// + /// Called after promoting staging font atlas to the actual atlas for .
+ /// Expect to be passed.
+ ///
+ /// This callback is not guaranteed to happen after , + /// but it will never happen on its own. + ///
+ PostPromotion, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs new file mode 100644 index 000000000..4f5b34061 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs @@ -0,0 +1,15 @@ +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Delegate to be called when a font needs to be built. +/// +/// A toolkit that may help you for font building steps. +/// +/// An implementation of may implement all of +/// , , and +/// .
+/// Either use to identify the build step, or use +/// , , +/// and for routing. +///
+public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs new file mode 100644 index 000000000..586887a3b --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Convenience function for building fonts through . +/// +public static class FontAtlasBuildToolkitUtilities +{ + /// + /// Compiles given s into an array of containing ImGui glyph ranges. + /// + /// The chars. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this IEnumerable enumerable, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); + foreach (var c in enumerable) + builder.AddChar(c); + return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); + } + + /// + /// Compiles given s into an array of containing ImGui glyph ranges. + /// + /// The chars. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this ReadOnlySpan span, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); + foreach (var c in span) + builder.AddChar(c); + return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); + } + + /// + /// Compiles given string into an array of containing ImGui glyph ranges. + /// + /// The string. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this string @string, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) => + @string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints); + + /// + /// Finds the corresponding in + /// . that corresponds to the + /// specified font . + /// + /// The toolkit. + /// The font. + /// The relevant config pointer, or empty config pointer if not found. + public static unsafe ImFontConfigPtr FindConfigPtr(this IFontAtlasBuildToolkit toolkit, ImFontPtr fontPtr) + { + foreach (ref var c in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan) + { + if (c.DstFont == fontPtr.NativePtr) + return new((nint)Unsafe.AsPointer(ref c)); + } + + return default; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// This, for method chaining. + public static IFontAtlasBuildToolkit OnPreBuild( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PreBuild) + action.Invoke((IFontAtlasBuildToolkitPreBuild)toolkit); + return toolkit; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// toolkit, for method chaining. + public static IFontAtlasBuildToolkit OnPostBuild( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PostBuild) + action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit); + return toolkit; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// toolkit, for method chaining. + public static IFontAtlasBuildToolkit OnPostPromotion( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion) + action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit); + return toolkit; + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs new file mode 100644 index 000000000..ec3e66e9a --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -0,0 +1,141 @@ +using System.Threading.Tasks; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Wrapper for . +/// +public interface IFontAtlas : IDisposable +{ + /// + /// Event to be called on build step changes.
+ /// is meaningless for this event. + ///
+ event FontAtlasBuildStepDelegate? BuildStepChange; + + /// + /// Event fired when a font rebuild operation is recommended.
+ /// This event will be invoked from the main thread.
+ ///
+ /// Reasons for the event include changes in and + /// initialization of new associated font handles. + ///
+ /// + /// You should call or + /// if is not set to true.
+ /// Avoid calling here; it will block the main thread. + ///
+ event Action? RebuildRecommend; + + /// + /// Gets the name of the atlas. For logging and debugging purposes. + /// + string Name { get; } + + /// + /// Gets a value how the atlas should be rebuilt when the relevant Dalamud Configuration changes. + /// + FontAtlasAutoRebuildMode AutoRebuildMode { get; } + + /// + /// Gets the font atlas. Might be empty. + /// + ImFontAtlasPtr ImAtlas { get; } + + /// + /// Gets the task that represents the current font rebuild state. + /// + Task BuildTask { get; } + + /// + /// Gets a value indicating whether there exists any built atlas, regardless of . + /// + bool HasBuiltAtlas { get; } + + /// + /// Gets a value indicating whether this font atlas is under the effect of global scale. + /// + bool IsGlobalScaled { get; } + + /// + /// Suppresses automatically rebuilding fonts for the scope. + /// + /// An instance of that will release the suppression. + /// + /// Use when you will be creating multiple new handles, and want rebuild to trigger only when you're done doing so. + /// This function will effectively do nothing, if is set to + /// . + /// + /// + /// + /// using (atlas.SuppressBuild()) { + /// this.font1 = atlas.NewGameFontHandle(...); + /// this.font2 = atlas.NewDelegateFontHandle(...); + /// } + /// + /// + public IDisposable SuppressAutoRebuild(); + + /// + /// Creates a new from game's built-in fonts. + /// + /// Font to use. + /// Handle to a font that may or may not be ready yet. + public IFontHandle NewGameFontHandle(GameFontStyle style); + + /// + /// Creates a new IFontHandle using your own callbacks. + /// + /// Callback for . + /// Handle to a font that may or may not be ready yet. + /// + /// On initialization: + /// + /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => { + /// var config = new SafeFontConfig { SizePx = 16 }; + /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); + /// tk.AddGameSymbol(config); + /// tk.AddExtraGlyphsForDalamudLanguage(config); + /// // optionally do the following if you have to add more than one font here, + /// // to specify which font added during this delegate is the final font to use. + /// tk.Font = config.MergeFont; + /// })); + /// // or + /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); + /// + ///
+ /// On use: + /// + /// using (this.fontHandle.Push()) + /// ImGui.TextUnformatted("Example"); + /// + ///
+ public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate); + + /// + /// Queues rebuilding fonts, on the main thread.
+ /// Note that would not necessarily get changed from calling this function. + ///
+ /// If is . + void BuildFontsOnNextFrame(); + + /// + /// Rebuilds fonts immediately, on the current thread.
+ /// Even the callback for will be called on the same thread. + ///
+ /// If is . + void BuildFontsImmediately(); + + /// + /// Rebuilds fonts asynchronously, on any thread. + /// + /// Call on the main thread. + /// The task. + /// If is . + Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs new file mode 100644 index 000000000..4b016bbb2 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -0,0 +1,67 @@ +using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Common stuff for and . +/// +public interface IFontAtlasBuildToolkit +{ + /// + /// Gets or sets the font relevant to the call. + /// + ImFontPtr Font { get; set; } + + /// + /// Gets the current scale this font atlas is being built with. + /// + float Scale { get; } + + /// + /// Gets a value indicating whether the current build operation is asynchronous. + /// + bool IsAsyncBuildOperation { get; } + + /// + /// Gets the current build step. + /// + FontAtlasBuildStep BuildStep { get; } + + /// + /// Gets the font atlas being built. + /// + ImFontAtlasPtr NewImAtlas { get; } + + /// + /// Gets the wrapper for of .
+ /// This does not need to be disposed. Calling does nothing.- + ///
+ /// Modification of this vector may result in undefined behaviors. + ///
+ ImVectorWrapper Fonts { get; } + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// Disposable type. + /// The disposable. + /// The same . + T DisposeWithAtlas(T disposable) where T : IDisposable; + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// The gc handle. + /// The same . + GCHandle DisposeWithAtlas(GCHandle gcHandle); + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// The action to run on dispose. + void DisposeWithAtlas(Action action); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs new file mode 100644 index 000000000..3c14197e0 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -0,0 +1,26 @@ +using Dalamud.Interface.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is . +/// +public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit +{ + /// + /// Gets whether global scaling is ignored for the given font. + /// + /// The font. + /// True if ignored. + bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + + /// + /// Stores a texture to be managed with the atlas. + /// + /// The texture wrap. + /// Dispose the wrap on error. + /// The texture index. + int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs new file mode 100644 index 000000000..8c3c91624 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs @@ -0,0 +1,33 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is . +/// +public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit +{ + /// + /// Copies glyphs across fonts, in a safer way.
+ /// If the font does not belong to the current atlas, this function is a no-op. + ///
+ /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE'); + + /// + /// Calls , with some fixups. + /// + /// The font. + void BuildLookupTable(ImFontPtr font); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs new file mode 100644 index 000000000..cb8a27a54 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -0,0 +1,186 @@ +using System.IO; +using System.Runtime.InteropServices; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is .
+///
+/// After returns, +/// either must be set, +/// or at least one font must have been added to the atlas using one of AddFont... functions. +///
+public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit +{ + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// Disposable type. + /// The disposable. + /// The same . + T DisposeAfterBuild(T disposable) where T : IDisposable; + + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// The gc handle. + /// The same . + GCHandle DisposeAfterBuild(GCHandle gcHandle); + + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// The action to run on dispose. + void DisposeAfterBuild(Action action); + + /// + /// Excludes given font from global scaling. + /// + /// The font. + /// Same with . + ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr); + + /// + /// Gets whether global scaling is ignored for the given font. + /// + /// The font. + /// True if ignored. + bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + + /// + /// Adds a font from memory region allocated using .
+ /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// + /// Do NOT call on the once this function has + /// been called, unless is set and the function has thrown an error. + /// + ///
+ /// Memory address for the data allocated using . + /// The size of the font file.. + /// The font config. + /// Free if an exception happens. + /// A debug tag. + /// The newly added font. + unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + nint dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag) + => this.AddFontFromImGuiHeapAllocatedMemory( + (void*)dataPointer, + dataSize, + fontConfig, + freeOnException, + debugTag); + + /// + /// Adds a font from memory region allocated using .
+ /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// Do NOT call on the once this + /// function has been called. + ///
+ /// Memory address for the data allocated using . + /// The size of the font file.. + /// The font config. + /// Free if an exception happens. + /// A debug tag. + /// The newly added font. + unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + void* dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag); + + /// + /// Adds a font from a file. + /// + /// The file path to create a new font from. + /// The font config. + /// The newly added font. + ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig); + + /// + /// Adds a font from a stream. + /// + /// The stream to create a new font from. + /// The font config. + /// Dispose when this function returns or throws. + /// A debug tag. + /// The newly added font. + ImFontPtr AddFontFromStream(Stream stream, in SafeFontConfig fontConfig, bool leaveOpen, string debugTag); + + /// + /// Adds a font from memory. + /// + /// The span to create from. + /// The font config. + /// A debug tag. + /// The newly added font. + ImFontPtr AddFontFromMemory(ReadOnlySpan span, in SafeFontConfig fontConfig, string debugTag); + + /// + /// Adds the default font known to the current font atlas.
+ ///
+ /// Includes and .
+ /// As this involves adding multiple fonts, calling this function will set + /// as the return value of this function, if it was empty before. + ///
+ /// Font size in pixels. + /// The glyph ranges. Use .ToGlyphRange to build. + /// A font returned from . + ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null); + + /// + /// Adds a font that is shipped with Dalamud.
+ ///
+ /// Note: if game symbols font file is requested but is unavailable, + /// then it will take the glyphs from game's built-in fonts, and everything in + /// will be ignored but , , + /// and . + ///
+ /// The font type. + /// The font config. + /// The added font. + ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig); + + /// + /// Same with (, ...), + /// but using only FontAwesome icon ranges.
+ /// will be ignored. + ///
+ /// The font config. + /// The added font. + ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig); + + /// + /// Adds the game's symbols into the provided font.
+ /// will be ignored.
+ /// If the game symbol font file is unavailable, only will be honored. + ///
+ /// The font config. + /// The added font. + ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig); + + /// + /// Adds the game glyphs to the font. + /// + /// The font style. + /// The glyph ranges. + /// The font to merge to. If empty, then a new font will be created. + /// The added font. + ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont); + + /// + /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
+ /// will be ignored. + ///
+ /// The font config. + void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs new file mode 100644 index 000000000..854594663 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -0,0 +1,42 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Represents a reference counting handle for fonts. +/// +public interface IFontHandle : IDisposable +{ + /// + /// Represents a reference counting handle for fonts. Dalamud internal use only. + /// + internal interface IInternal : IFontHandle + { + /// + /// Gets the font.
+ /// Use of this properly is safe only from the UI thread.
+ /// Use if the intended purpose of this property is .
+ /// Futures changes may make simple not enough. + ///
+ ImFontPtr ImFont { get; } + } + + /// + /// Gets the load exception, if it failed to load. Otherwise, it is null. + /// + Exception? LoadException { get; } + + /// + /// Gets a value indicating whether this font is ready for use.
+ /// Use directly if you want to keep the current ImGui font if the font is not ready. + ///
+ bool Available { get; } + + /// + /// Pushes the current font into ImGui font stack using , if available.
+ /// Use to access the current font.
+ /// You may not access the font once you dispose this object. + ///
+ /// A disposable object that will call (1) on dispose. + IDisposable Push(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs new file mode 100644 index 000000000..f0ed09155 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -0,0 +1,334 @@ +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Logging.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// A font handle representing a user-callback generated font. +/// +internal class DelegateFontHandle : IFontHandle.IInternal +{ + private IFontHandleManager? manager; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Callback for . + public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange) + { + this.manager = manager; + this.CallOnBuildStepChange = callOnBuildStepChange; + } + + /// + /// Gets the function to be called on build step changes. + /// + public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; } + + /// + public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); + + /// + public bool Available => this.ImFont.IsNotNullAndLoaded(); + + /// + public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; + + private IFontHandleManager ManagerNotDisposed => + this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); + + /// + public void Dispose() + { + this.manager?.FreeFontHandle(this); + this.manager = null; + } + + /// + public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + + /// + /// Manager for s. + /// + internal sealed class HandleManager : IFontHandleManager + { + private readonly HashSet handles = new(); + private readonly object syncRoot = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the owner atlas. + public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager"; + + /// + public event Action? RebuildRecommend; + + /// + public string Name { get; } + + /// + public IFontHandleSubstance? Substance { get; set; } + + /// + public void Dispose() + { + lock (this.syncRoot) + { + this.handles.Clear(); + this.Substance?.Dispose(); + this.Substance = null; + } + } + + /// + public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) + { + var key = new DelegateFontHandle(this, buildStepDelegate); + lock (this.syncRoot) + this.handles.Add(key); + this.RebuildRecommend?.Invoke(); + return key; + } + + /// + public void FreeFontHandle(IFontHandle handle) + { + if (handle is not DelegateFontHandle cgfh) + return; + + lock (this.syncRoot) + this.handles.Remove(cgfh); + } + + /// + public IFontHandleSubstance NewSubstance() + { + lock (this.syncRoot) + return new HandleSubstance(this, this.handles.ToArray()); + } + } + + /// + /// Substance from . + /// + internal sealed class HandleSubstance : IFontHandleSubstance + { + private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); + + // Not owned by this class. Do not dispose. + private readonly DelegateFontHandle[] relevantHandles; + + // Owned by this class, but ImFontPtr values still do not belong to this. + private readonly Dictionary fonts = new(); + private readonly Dictionary buildExceptions = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The manager. + /// The relevant handles. + public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) + { + this.Manager = manager; + this.relevantHandles = relevantHandles; + } + + /// + public IFontHandleManager Manager { get; } + + /// + public void Dispose() + { + this.fonts.Clear(); + this.buildExceptions.Clear(); + } + + /// + public ImFontPtr GetFontPtr(IFontHandle handle) => + handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default; + + /// + public Exception? GetBuildException(IFontHandle handle) => + handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default; + + /// + public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + var fontsVector = toolkitPreBuild.Fonts; + foreach (var k in this.relevantHandles) + { + var fontCountPrevious = fontsVector.Length; + + try + { + toolkitPreBuild.Font = default; + k.CallOnBuildStepChange(toolkitPreBuild); + if (toolkitPreBuild.Font.IsNull()) + { + if (fontCountPrevious == fontsVector.Length) + { + throw new InvalidOperationException( + $"{nameof(FontAtlasBuildStepDelegate)} must either set the " + + $"{nameof(IFontAtlasBuildToolkitPreBuild.Font)} property, or add at least one font."); + } + + toolkitPreBuild.Font = fontsVector[^1]; + } + else + { + var found = false; + unsafe + { + for (var i = fontCountPrevious; !found && i < fontsVector.Length; i++) + { + if (fontsVector[i].NativePtr == toolkitPreBuild.Font.NativePtr) + found = true; + } + } + + if (!found) + { + throw new InvalidOperationException( + "The font does not exist in the atlas' font array. If you need an empty font, try" + + "adding Noto Sans from Dalamud Assets, but using new ushort[]{ ' ', ' ', 0 } as the" + + "glyph range."); + } + } + + if (fontsVector.Length - fontCountPrevious != 1) + { + Log.Warning( + "[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " + + "Using the most recently added font. " + + "Did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", + this.Manager.Name, + fontsVector.Length - fontCountPrevious, + nameof(FontAtlasBuildStepDelegate), + nameof(SafeFontConfig), + nameof(SafeFontConfig.MergeFont), + nameof(ImFontConfigPtr), + nameof(ImFontConfigPtr.MergeMode)); + } + + for (var i = fontCountPrevious; i < fontsVector.Length; i++) + { + if (fontsVector[i].ValidateUnsafe() is { } ex) + { + throw new InvalidOperationException( + "One of the newly added fonts seem to be pointing to an invalid memory address.", + ex); + } + } + + // Check for duplicate entries; duplicates will result in free-after-free + for (var i = 0; i < fontCountPrevious; i++) + { + for (var j = fontCountPrevious; j < fontsVector.Length; j++) + { + unsafe + { + if (fontsVector[i].NativePtr == fontsVector[j].NativePtr) + throw new InvalidOperationException("An already added font has been added again."); + } + } + } + + this.fonts[k] = toolkitPreBuild.Font; + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}:Substance] An error has occurred while during {delegate} PreBuild call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + + // Sanitization, in a futile attempt to prevent crashes on invalid parameters + unsafe + { + var distinct = + fontsVector + .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates + .Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them + .ToArray(); + + // We're adding the contents back; do not destroy the contents + fontsVector.Clear(true); + fontsVector.AddRange(distinct.AsSpan()); + } + } + } + } + + /// + public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + // irrelevant + } + + /// + public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + foreach (var k in this.relevantHandles) + { + if (!this.fonts[k].IsNotNullAndLoaded()) + continue; + + try + { + toolkitPostBuild.Font = this.fonts[k]; + k.CallOnBuildStepChange.Invoke(toolkitPostBuild); + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}] An error has occurred while during {delegate} PostBuild call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + } + } + } + + /// + public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) + { + foreach (var k in this.relevantHandles) + { + if (!this.fonts[k].IsNotNullAndLoaded()) + continue; + + try + { + toolkitPostPromotion.Font = this.fonts[k]; + k.CallOnBuildStepChange.Invoke(toolkitPostPromotion); + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs new file mode 100644 index 000000000..e73ea7548 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -0,0 +1,682 @@ +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Unicode; + +using Dalamud.Configuration.Internal; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using SharpDX.DXGI; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Standalone font atlas. +/// +internal sealed partial class FontAtlasFactory +{ + private static readonly Dictionary> PairAdjustmentsCache = + new(); + + /// + /// Implementations for and + /// . + /// + private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable + { + private static readonly ushort FontAwesomeIconMin = + (ushort)Enum.GetValues().Where(x => x > 0).Min(); + + private static readonly ushort FontAwesomeIconMax = + (ushort)Enum.GetValues().Where(x => x > 0).Max(); + + private readonly DisposeSafety.ScopedFinalizer disposeAfterBuild = new(); + private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance; + private readonly FontAtlasFactory factory; + private readonly FontAtlasBuiltData data; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// New atlas. + /// An instance of . + /// Specify whether the current build operation is an asynchronous one. + public BuildToolkit( + FontAtlasFactory factory, + FontAtlasBuiltData data, + GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance, + bool isAsync) + { + this.data = data; + this.gameFontHandleSubstance = gameFontHandleSubstance; + this.IsAsyncBuildOperation = isAsync; + this.factory = factory; + } + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.data.Scale; + + /// + public bool IsAsyncBuildOperation { get; } + + /// + public FontAtlasBuildStep BuildStep { get; set; } + + /// + public ImFontAtlasPtr NewImAtlas => this.data.Atlas; + + /// + public ImVectorWrapper Fonts => this.data.Fonts; + + /// + /// Gets the list of fonts to ignore global scale. + /// + public List GlobalScaleExclusions { get; } = new(); + + /// + public void Dispose() => this.disposeAfterBuild.Dispose(); + + /// + public T2 DisposeAfterBuild(T2 disposable) where T2 : IDisposable => + this.disposeAfterBuild.Add(disposable); + + /// + public GCHandle DisposeAfterBuild(GCHandle gcHandle) => this.disposeAfterBuild.Add(gcHandle); + + /// + public void DisposeAfterBuild(Action action) => this.disposeAfterBuild.Add(action); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.data.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.data.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action); + + /// + public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) + { + this.GlobalScaleExclusions.Add(fontPtr); + return fontPtr; + } + + /// + public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => + this.GlobalScaleExclusions.Contains(fontPtr); + + /// + public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => + this.data.AddNewTexture(textureWrap, disposeOnError); + + /// + public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + void* dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag) + { + Log.Verbose( + "[{name}] 0x{atlas:X}: {funcname}(0x{dataPointer:X}, 0x{dataSize:X}, ...) from {tag}", + this.data.Owner?.Name ?? "(error)", + (nint)this.NewImAtlas.NativePtr, + nameof(this.AddFontFromImGuiHeapAllocatedMemory), + (nint)dataPointer, + dataSize, + debugTag); + + try + { + fontConfig.ThrowOnInvalidValues(); + + var raw = fontConfig.Raw with + { + FontData = dataPointer, + FontDataSize = dataSize, + }; + + if (fontConfig.GlyphRanges is not { Length: > 0 } ranges) + ranges = new ushort[] { 1, 0xFFFE, 0 }; + + raw.GlyphRanges = (ushort*)this.DisposeAfterBuild( + GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject(); + + TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw); + + var font = this.NewImAtlas.AddFont(&raw); + + var dataHash = default(HashCode); + dataHash.AddBytes(new(dataPointer, dataSize)); + var hashIdent = (uint)dataHash.ToHashCode() | ((ulong)dataSize << 32); + + List<(char Left, char Right, float Distance)> pairAdjustments; + lock (PairAdjustmentsCache) + { + if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments)) + { + PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new()); + try + { + pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray()); + } + catch + { + // don't care + } + } + } + + foreach (var pair in pairAdjustments) + { + if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Left, raw.GlyphRanges)) + continue; + if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Right, raw.GlyphRanges)) + continue; + + font.AddKerningPair(pair.Left, pair.Right, pair.Distance * raw.SizePixels); + } + + return font; + } + catch + { + if (freeOnException) + ImGuiNative.igMemFree(dataPointer); + throw; + } + } + + /// + public ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig) + { + return this.AddFontFromStream( + File.OpenRead(path), + fontConfig, + false, + $"{nameof(this.AddFontFromFile)}({path})"); + } + + /// + public unsafe ImFontPtr AddFontFromStream( + Stream stream, + in SafeFontConfig fontConfig, + bool leaveOpen, + string debugTag) + { + using var streamCloser = leaveOpen ? null : stream; + if (!stream.CanSeek) + { + // There is no need to dispose a MemoryStream. + var ms = new MemoryStream(); + stream.CopyTo(ms); + stream = ms; + } + + var length = checked((int)(uint)stream.Length); + var memory = ImGuiHelpers.AllocateMemory(length); + try + { + stream.ReadExactly(new(memory, length)); + return this.AddFontFromImGuiHeapAllocatedMemory( + memory, + length, + fontConfig, + false, + $"{nameof(this.AddFontFromStream)}({debugTag})"); + } + catch + { + ImGuiNative.igMemFree(memory); + throw; + } + } + + /// + public unsafe ImFontPtr AddFontFromMemory( + ReadOnlySpan span, + in SafeFontConfig fontConfig, + string debugTag) + { + var length = span.Length; + var memory = ImGuiHelpers.AllocateMemory(length); + try + { + span.CopyTo(new(memory, length)); + return this.AddFontFromImGuiHeapAllocatedMemory( + memory, + length, + fontConfig, + false, + $"{nameof(this.AddFontFromMemory)}({debugTag})"); + } + catch + { + ImGuiNative.igMemFree(memory); + throw; + } + } + + /// + public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges) + { + ImFontPtr font; + glyphRanges ??= this.factory.DefaultGlyphRanges; + if (this.factory.UseAxis) + { + font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); + } + else + { + font = this.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() { SizePx = sizePx, GlyphRanges = glyphRanges }); + this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font }); + } + + this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font }); + if (this.Font.IsNull()) + this.Font = font; + return font; + } + + /// + public ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig) + { + if (asset.GetPurpose() != DalamudAssetPurpose.Font) + throw new ArgumentOutOfRangeException(nameof(asset), asset, "Must have the purpose of Font."); + + switch (asset) + { + case DalamudAsset.LodestoneGameSymbol when this.factory.HasGameSymbolsFontFile: + return this.factory.AddFont( + this, + asset, + fontConfig with + { + FontNo = 0, + SizePx = (fontConfig.SizePx * 3) / 2, + }); + + case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile: + { + return this.AddGameGlyphs( + new(GameFontFamily.Axis, fontConfig.SizePx), + fontConfig.GlyphRanges, + fontConfig.MergeFont); + } + + default: + return this.factory.AddFont( + this, + asset, + fontConfig with + { + FontNo = 0, + }); + } + } + + /// + public ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( + DalamudAsset.FontAwesomeFreeSolid, + fontConfig with + { + GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 }, + }); + + /// + public ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig) => + this.AddDalamudAssetFont( + DalamudAsset.LodestoneGameSymbol, + fontConfig with + { + GlyphRanges = new ushort[] + { + GamePrebakedFontHandle.SeIconCharMin, + GamePrebakedFontHandle.SeIconCharMax, + 0, + }, + }); + + /// + public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) => + this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges); + + /// + public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) + { + var dalamudConfiguration = Service.Get(); + if (dalamudConfiguration.EffectiveLanguage == "ko" + || Service.GetNullable()?.EncounteredHangul is true) + { + this.AddDalamudAssetFont( + DalamudAsset.NotoSansKrRegular, + fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.HangulJamo, + UnicodeRanges.HangulCompatibilityJamo, + UnicodeRanges.HangulSyllables, + UnicodeRanges.HangulJamoExtendedA, + UnicodeRanges.HangulJamoExtendedB), + }); + } + + var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc"); + if (!File.Exists(fontPathChs)) + fontPathChs = null; + + var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc"); + if (!File.Exists(fontPathCht)) + fontPathCht = null; + + if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") + { + this.AddFontFromFile(fontPathCht, fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkUnifiedIdeographsExtensionA), + }); + } + else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" + || Service.GetNullable()?.EncounteredHan is true)) + { + this.AddFontFromFile(fontPathChs, fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkUnifiedIdeographsExtensionA), + }); + } + } + + public void PreBuildSubstances() + { + foreach (var substance in this.data.Substances) + substance.OnPreBuild(this); + foreach (var substance in this.data.Substances) + substance.OnPreBuildCleanup(this); + } + + public unsafe void PreBuild() + { + var configData = this.data.ConfigData; + foreach (ref var config in configData.DataSpan) + { + if (this.GlobalScaleExclusions.Contains(new(config.DstFont))) + continue; + + config.SizePixels *= this.Scale; + + config.GlyphMaxAdvanceX *= this.Scale; + if (float.IsInfinity(config.GlyphMaxAdvanceX)) + config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue; + + config.GlyphMinAdvanceX *= this.Scale; + if (float.IsInfinity(config.GlyphMinAdvanceX)) + config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue; + + config.GlyphOffset *= this.Scale; + } + } + + public void DoBuild() + { + // ImGui will call AddFontDefault() on Build() call. + // AddFontDefault() will reliably crash, when invoked multithreaded. + // We add a dummy font to prevent that. + if (this.data.ConfigData.Length == 0) + { + this.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 }); + } + + if (!this.NewImAtlas.Build()) + throw new InvalidOperationException("ImFontAtlas.Build failed"); + + this.BuildStep = FontAtlasBuildStep.PostBuild; + } + + public unsafe void PostBuild() + { + var scale = this.Scale; + foreach (ref var font in this.Fonts.DataSpan) + { + if (!this.GlobalScaleExclusions.Contains(font)) + font.AdjustGlyphMetrics(1 / scale, 1 / scale); + + foreach (var c in FallbackCodepoints) + { + var g = font.FindGlyphNoFallback(c); + if (g.NativePtr == null) + continue; + + font.UpdateFallbackChar(c); + break; + } + + foreach (var c in EllipsisCodepoints) + { + var g = font.FindGlyphNoFallback(c); + if (g.NativePtr == null) + continue; + + font.EllipsisChar = c; + break; + } + } + } + + public void PostBuildSubstances() + { + foreach (var substance in this.data.Substances) + substance.OnPostBuild(this); + } + + public unsafe void UploadTextures() + { + var buf = Array.Empty(); + try + { + var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var bpp = use4 ? 2 : 4; + var width = this.NewImAtlas.TexWidth; + var height = this.NewImAtlas.TexHeight; + foreach (ref var texture in this.data.ImTextures.DataSpan) + { + if (texture.TexID != 0) + { + // Nothing to do + } + else if (texture.TexPixelsRGBA32 is not null) + { + var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + new(texture.TexPixelsRGBA32, width * height * 4), + width * 4, + width, + height, + use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm); + this.data.AddExistingTexture(wrap); + texture.TexID = wrap.ImGuiHandle; + } + else if (texture.TexPixelsAlpha8 is not null) + { + var numPixels = width * height; + if (buf.Length < numPixels * bpp) + { + ArrayPool.Shared.Return(buf); + buf = ArrayPool.Shared.Rent(numPixels * bpp); + } + + fixed (void* pBuf = buf) + { + var sourcePtr = texture.TexPixelsAlpha8; + if (use4) + { + var target = (ushort*)pBuf; + while (numPixels-- > 0) + { + *target = (ushort)((*sourcePtr << 8) | 0x0FFF); + target++; + sourcePtr++; + } + } + else + { + var target = (uint*)pBuf; + while (numPixels-- > 0) + { + *target = (uint)((*sourcePtr << 24) | 0x00FFFFFF); + target++; + sourcePtr++; + } + } + } + + var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + buf, + width * bpp, + width, + height, + use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm); + this.data.AddExistingTexture(wrap); + texture.TexID = wrap.ImGuiHandle; + continue; + } + else + { + Log.Warning( + "[{name}]: TexID, TexPixelsRGBA32, and TexPixelsAlpha8 are all null", + this.data.Owner?.Name ?? "(error)"); + } + + if (texture.TexPixelsRGBA32 is not null) + ImGuiNative.igMemFree(texture.TexPixelsRGBA32); + if (texture.TexPixelsAlpha8 is not null) + ImGuiNative.igMemFree(texture.TexPixelsAlpha8); + texture.TexPixelsRGBA32 = null; + texture.TexPixelsAlpha8 = null; + } + } + finally + { + ArrayPool.Shared.Return(buf); + } + } + } + + /// + /// Implementations for . + /// + private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion + { + private readonly FontAtlasBuiltData builtData; + + /// + /// Initializes a new instance of the class. + /// + /// The built data. + public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.builtData.Scale; + + /// + public bool IsAsyncBuildOperation => true; + + /// + public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; + + /// + public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; + + /// + public unsafe ImVectorWrapper Fonts => new( + &this.NewImAtlas.NativePtr->Fonts, + x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); + + /// + public unsafe void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE') + { + var sourceFound = false; + var targetFound = false; + foreach (var f in this.Fonts) + { + sourceFound |= f.NativePtr == source.NativePtr; + targetFound |= f.NativePtr == target.NativePtr; + } + + if (sourceFound && targetFound) + { + ImGuiHelpers.CopyGlyphsAcrossFonts( + source, + target, + missingOnly, + false, + rangeLow, + rangeHigh); + if (rebuildLookupTable) + this.BuildLookupTable(target); + } + } + + /// + public unsafe void BuildLookupTable(ImFontPtr font) + { + // Need to clear previous Fallback pointers before BuildLookupTable, or it may crash + font.NativePtr->FallbackGlyph = null; + font.NativePtr->FallbackHotData = null; + font.BuildLookupTable(); + + // Need to fix our custom ImGui, so that imgui_widgets.cpp:3656 stops thinking + // Codepoint < FallbackHotData.size always means that it's not fallback char. + // Otherwise, having a fallback character in ImGui.InputText gets strange. + var indexedHotData = font.IndexedHotDataWrapped(); + var indexLookup = font.IndexLookupWrapped(); + ref var fallbackHotData = ref *(ImGuiHelpers.ImFontGlyphHotDataReal*)font.NativePtr->FallbackHotData; + for (var codepoint = 0; codepoint < indexedHotData.Length; codepoint++) + { + if (indexLookup[codepoint] == ushort.MaxValue) + { + indexedHotData[codepoint].AdvanceX = fallbackHotData.AdvanceX; + indexedHotData[codepoint].OccupiedWidth = fallbackHotData.OccupiedWidth; + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs new file mode 100644 index 000000000..5656fc673 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -0,0 +1,726 @@ +// #define VeryVerboseLog + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +using ImGuiNET; + +using JetBrains.Annotations; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Standalone font atlas. +/// +internal sealed partial class FontAtlasFactory +{ + /// + /// Fallback codepoints for ImFont. + /// + public const string FallbackCodepoints = "\u3013\uFFFD?-"; + + /// + /// Ellipsis codepoints for ImFont. + /// + public const string EllipsisCodepoints = "\u2026\u0085"; + + /// + /// If set, disables concurrent font build operation. + /// + private static readonly object? NoConcurrentBuildOperationLock = null; // new(); + + private static readonly ModuleLog Log = new(nameof(FontAtlasFactory)); + + private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); + + private struct FontAtlasBuiltData : IDisposable + { + public readonly DalamudFontAtlas? Owner; + public readonly ImFontAtlasPtr Atlas; + public readonly float Scale; + + public bool IsBuildInProgress; + + private readonly List? wraps; + private readonly List? substances; + private readonly DisposeSafety.ScopedFinalizer? garbage; + + public unsafe FontAtlasBuiltData( + DalamudFontAtlas owner, + IEnumerable substances, + float scale) + { + this.Owner = owner; + this.Scale = scale; + this.garbage = new(); + + try + { + var substancesList = this.substances = new(); + foreach (var s in substances) + substancesList.Add(this.garbage.Add(s)); + this.garbage.Add(() => substancesList.Clear()); + + var wrapsCopy = this.wraps = new(); + this.garbage.Add(() => wrapsCopy.Clear()); + + var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas(); + this.Atlas = atlasPtr; + if (this.Atlas.NativePtr is null) + throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}."); + + this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); + this.IsBuildInProgress = true; + } + catch + { + this.garbage.Dispose(); + throw; + } + } + + public readonly DisposeSafety.ScopedFinalizer Garbage => + this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + public readonly ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); + + public readonly ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + + public readonly ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + + public readonly IReadOnlyList Wraps => + (IReadOnlyList?)this.wraps ?? Array.Empty(); + + public readonly IReadOnlyList Substances => + (IReadOnlyList?)this.substances ?? Array.Empty(); + + public readonly void AddExistingTexture(IDalamudTextureWrap wrap) + { + if (this.wraps is null) + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + this.wraps.Add(this.Garbage.Add(wrap)); + } + + public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) + { + if (this.wraps is null) + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + var handle = wrap.ImGuiHandle; + var index = this.ImTextures.IndexOf(x => x.TexID == handle); + if (index == -1) + { + try + { + this.wraps.EnsureCapacity(this.wraps.Count + 1); + this.ImTextures.EnsureCapacityExponential(this.ImTextures.Length + 1); + + index = this.ImTextures.Length; + this.wraps.Add(this.Garbage.Add(wrap)); + this.ImTextures.Add(new() { TexID = handle }); + } + catch (Exception e) + { + if (disposeOnError) + wrap.Dispose(); + + if (this.wraps.Count != this.ImTextures.Length) + { + Log.Error( + e, + "{name} failed, and {wraps} and {imtextures} have different number of items", + nameof(this.AddNewTexture), + nameof(this.Wraps), + nameof(this.ImTextures)); + + if (this.wraps.Count > 0 && this.wraps[^1] == wrap) + this.wraps.RemoveAt(this.wraps.Count - 1); + if (this.ImTextures.Length > 0 && this.ImTextures[^1].TexID == handle) + this.ImTextures.RemoveAt(this.ImTextures.Length - 1); + + if (this.wraps.Count != this.ImTextures.Length) + Log.Fatal("^ Failed to undo due to an internal inconsistency; embrace for a crash"); + } + + throw; + } + } + + return index; + } + + public unsafe void Dispose() + { + if (this.garbage is null) + return; + + if (this.IsBuildInProgress) + { + Log.Error( + "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + + "Stack:\n{trace}", + this.Owner?.Name ?? "", + (nint)this.Atlas.NativePtr, + new StackTrace()); + while (this.IsBuildInProgress) + Thread.Sleep(100); + } + +#if VeryVerboseLog + Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); +#endif + this.garbage.Dispose(); + } + + public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) + { + var axisSubstance = this.Substances.OfType().Single(); + return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild }; + } + } + + private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback + { + private readonly DisposeSafety.ScopedFinalizer disposables = new(); + private readonly FontAtlasFactory factory; + private readonly DelegateFontHandle.HandleManager delegateFontHandleManager; + private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager; + private readonly IFontHandleManager[] fontHandleManagers; + + private readonly object syncRootPostPromotion = new(); + private readonly object syncRoot = new(); + + private Task buildTask = EmptyTask; + private FontAtlasBuiltData builtData; + + private int buildSuppressionCounter; + private bool buildSuppressionSuppressed; + + private int buildIndex; + private bool buildQueued; + private bool disposed = false; + + /// + /// Initializes a new instance of the class. + /// + /// The factory. + /// Name of atlas, for debugging and logging purposes. + /// Specify how to auto rebuild. + /// Whether the fonts in the atlas are under the effect of global scale. + public DalamudFontAtlas( + FontAtlasFactory factory, + string atlasName, + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled) + { + this.IsGlobalScaled = isGlobalScaled; + try + { + this.factory = factory; + this.AutoRebuildMode = autoRebuildMode; + this.Name = atlasName; + + this.factory.InterfaceManager.AfterBuildFonts += this.OnRebuildRecommend; + this.disposables.Add(() => this.factory.InterfaceManager.AfterBuildFonts -= this.OnRebuildRecommend); + + this.fontHandleManagers = new IFontHandleManager[] + { + this.delegateFontHandleManager = this.disposables.Add( + new DelegateFontHandle.HandleManager(atlasName)), + this.gameFontHandleManager = this.disposables.Add( + new GamePrebakedFontHandle.HandleManager(atlasName, factory)), + }; + foreach (var fhm in this.fontHandleManagers) + fhm.RebuildRecommend += this.OnRebuildRecommend; + } + catch + { + this.disposables.Dispose(); + throw; + } + + this.factory.SceneTask.ContinueWith( + r => + { + lock (this.syncRoot) + { + if (this.disposed) + return; + + r.Result.OnNewRenderFrame += this.ImGuiSceneOnNewRenderFrame; + this.disposables.Add(() => r.Result.OnNewRenderFrame -= this.ImGuiSceneOnNewRenderFrame); + } + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) + this.BuildFontsOnNextFrame(); + }); + } + + /// + /// Finalizes an instance of the class. + /// + ~DalamudFontAtlas() + { + lock (this.syncRoot) + { + this.buildTask.ToDisposableIgnoreExceptions().Dispose(); + this.builtData.Dispose(); + } + } + + /// + public event FontAtlasBuildStepDelegate? BuildStepChange; + + /// + public event Action? RebuildRecommend; + + /// + public event Action? BeforeDispose; + + /// + public event Action? AfterDispose; + + /// + public string Name { get; } + + /// + public FontAtlasAutoRebuildMode AutoRebuildMode { get; } + + /// + public ImFontAtlasPtr ImAtlas + { + get + { + lock (this.syncRoot) + return this.builtData.Atlas; + } + } + + /// + public Task BuildTask => this.buildTask; + + /// + public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); + + /// + public bool IsGlobalScaled { get; } + + /// + public void Dispose() + { + if (this.disposed) + return; + + this.BeforeDispose?.InvokeSafely(this); + + try + { + lock (this.syncRoot) + { + this.disposed = true; + this.buildTask.ToDisposableIgnoreExceptions().Dispose(); + this.buildTask = EmptyTask; + this.disposables.Add(this.builtData); + this.builtData = default; + this.disposables.Dispose(); + } + + try + { + this.AfterDispose?.Invoke(this, null); + } + catch + { + // ignore + } + } + catch (Exception e) + { + try + { + this.AfterDispose?.Invoke(this, e); + } + catch + { + // ignore + } + } + + GC.SuppressFinalize(this); + } + + /// + public IDisposable SuppressAutoRebuild() + { + this.buildSuppressionCounter++; + return Disposable.Create( + () => + { + this.buildSuppressionCounter--; + if (this.buildSuppressionSuppressed) + this.OnRebuildRecommend(); + }); + } + + /// + public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); + + /// + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => + this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); + + /// + public void BuildFontsOnNextFrame() + { + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsOnNextFrame)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.Async)}."); + } + + if (!this.buildTask.IsCompleted || this.buildQueued) + return; + +#if VeryVerboseLog + Log.Verbose("[{name}] Queueing from {source}.", this.Name, nameof(this.BuildFontsOnNextFrame)); +#endif + + this.buildQueued = true; + } + + /// + public void BuildFontsImmediately() + { +#if VeryVerboseLog + Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsImmediately)); +#endif + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsImmediately)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.Async)}."); + } + + var tcs = new TaskCompletionSource(); + int rebuildIndex; + try + { + rebuildIndex = ++this.buildIndex; + lock (this.syncRoot) + { + if (!this.buildTask.IsCompleted) + throw new InvalidOperationException("Font rebuild is already in progress."); + + this.buildTask = tcs.Task; + } + +#if VeryVerboseLog + Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsImmediately)); +#endif + + var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; + var r = this.RebuildFontsPrivate(false, scale); + r.Wait(); + if (r.IsCompletedSuccessfully) + tcs.SetResult(r.Result); + else if (r.Exception is not null) + tcs.SetException(r.Exception); + else + tcs.SetCanceled(); + } + catch (Exception e) + { + tcs.SetException(e); + Log.Error(e, "[{name}] Failed to build fonts.", this.Name); + throw; + } + + this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately)); + } + + /// + public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true) + { +#if VeryVerboseLog + Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync)); +#endif + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsAsync)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.OnNewFrame)}."); + } + + lock (this.syncRoot) + { + var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; + var rebuildIndex = ++this.buildIndex; + return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); + + async Task BuildInner(Task unused) + { + Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync)); + lock (this.syncRoot) + { + if (this.buildIndex != rebuildIndex) + return default; + } + + var res = await this.RebuildFontsPrivate(true, scale); + if (res.Atlas.IsNull()) + return res; + + if (callPostPromotionOnMainThread) + { + await this.factory.Framework.RunOnFrameworkThread( + () => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync))); + } + else + { + this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); + } + + return res; + } + } + } + + private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) + { + lock (this.syncRoot) + { + if (this.buildIndex != rebuildIndex) + { + data.ExplicitDisposeIgnoreExceptions(); + return; + } + + this.builtData.ExplicitDisposeIgnoreExceptions(); + this.builtData = data; + this.buildTask = EmptyTask; + foreach (var substance in data.Substances) + substance.Manager.Substance = substance; + } + + lock (this.syncRootPostPromotion) + { + if (this.buildIndex != rebuildIndex) + { + data.ExplicitDisposeIgnoreExceptions(); + return; + } + + var toolkit = new BuildToolkitPostPromotion(data); + + try + { + this.BuildStepChange?.Invoke(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {delegateName} PostPromotion error", + this.Name, + nameof(FontAtlasBuildStepDelegate)); + } + + foreach (var substance in data.Substances) + { + try + { + substance.OnPostPromotion(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {substance} PostPromotion error", + this.Name, + substance.GetType().FullName ?? substance.GetType().Name); + } + } + + foreach (var font in toolkit.Fonts) + { + try + { + toolkit.BuildLookupTable(font); + } + catch (Exception e) + { + Log.Error(e, "[{name}] BuildLookupTable error", this.Name); + } + } + +#if VeryVerboseLog + Log.Verbose("[{name}] Built from {source}.", this.Name, source); +#endif + } + } + + private void ImGuiSceneOnNewRenderFrame() + { + if (!this.buildQueued) + return; + + try + { + if (this.AutoRebuildMode != FontAtlasAutoRebuildMode.Async) + this.BuildFontsImmediately(); + } + finally + { + this.buildQueued = false; + } + } + + private Task RebuildFontsPrivate(bool isAsync, float scale) + { + if (NoConcurrentBuildOperationLock is null) + return this.RebuildFontsPrivateReal(isAsync, scale); + lock (NoConcurrentBuildOperationLock) + return this.RebuildFontsPrivateReal(isAsync, scale); + } + + private async Task RebuildFontsPrivateReal(bool isAsync, float scale) + { + lock (this.syncRoot) + { + // this lock ensures that this.buildTask is properly set. + } + + var sw = new Stopwatch(); + sw.Start(); + + var res = default(FontAtlasBuiltData); + nint atlasPtr = 0; + try + { + res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + unsafe + { + atlasPtr = (nint)res.Atlas.NativePtr; + } + + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: PreBuild (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + + using var toolkit = res.CreateToolkit(this.factory, isAsync); + this.BuildStepChange?.Invoke(toolkit); + toolkit.PreBuildSubstances(); + toolkit.PreBuild(); + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + + toolkit.DoBuild(); + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: PostBuild (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + + toolkit.PostBuild(); + toolkit.PostBuildSubstances(); + this.BuildStepChange?.Invoke(toolkit); + + if (this.factory.SceneTask is { IsCompleted: false } sceneTask) + { + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: await SceneTask (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + await sceneTask.ConfigureAwait(!isAsync); + } + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: UploadTextures (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + toolkit.UploadTextures(); + + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: Complete (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + + res.IsBuildInProgress = false; + return res; + } + catch (Exception e) + { + Log.Error( + e, + "[{name}:{functionname}] 0x{ptr:X}: Failed (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + res.IsBuildInProgress = false; + res.Dispose(); + throw; + } + finally + { + this.buildQueued = false; + } + } + + private void OnRebuildRecommend() + { + if (this.disposed) + return; + + if (this.buildSuppressionCounter > 0) + { + this.buildSuppressionSuppressed = true; + return; + } + + this.buildSuppressionSuppressed = false; + this.factory.Framework.RunOnFrameworkThread( + () => + { + this.RebuildRecommend?.InvokeSafely(); + + switch (this.AutoRebuildMode) + { + case FontAtlasAutoRebuildMode.Async: + _ = this.BuildFontsAsync(); + break; + case FontAtlasAutoRebuildMode.OnNewFrame: + this.BuildFontsOnNextFrame(); + break; + case FontAtlasAutoRebuildMode.Disable: + default: + break; + } + }); + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs new file mode 100644 index 000000000..358ccd845 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -0,0 +1,368 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Configuration.Internal; +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using ImGuiScene; + +using Lumina.Data.Files; + +using SharpDX; +using SharpDX.Direct3D11; +using SharpDX.DXGI; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Factory for the implementation of . +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed partial class FontAtlasFactory + : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable +{ + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly IReadOnlyDictionary> fdtFiles; + private readonly IReadOnlyDictionary[]>> texFiles; + private readonly IReadOnlyDictionary> prebakedTextureWraps; + private readonly Task defaultGlyphRanges; + private readonly DalamudAssetManager dalamudAssetManager; + + [ServiceManager.ServiceConstructor] + private FontAtlasFactory( + DataManager dataManager, + Framework framework, + InterfaceManager interfaceManager, + DalamudAssetManager dalamudAssetManager) + { + this.Framework = framework; + this.InterfaceManager = interfaceManager; + this.dalamudAssetManager = dalamudAssetManager; + this.SceneTask = Service + .GetAsync() + .ContinueWith(r => r.Result.Manager.Scene); + + var gffasInfo = Enum.GetValues() + .Select( + x => + ( + Font: x, + Attr: x.GetAttribute())) + .Where(x => x.Attr is not null) + .ToArray(); + var texPaths = gffasInfo.Select(x => x.Attr.TexPathFormat).Distinct().ToArray(); + + this.fdtFiles = gffasInfo.ToImmutableDictionary( + x => x.Font, + x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); + var channelCountsTask = texPaths.ToImmutableDictionary( + x => x, + x => Task.WhenAll( + gffasInfo.Where(y => y.Attr.TexPathFormat == x) + .Select(y => this.fdtFiles[y.Font])) + .ContinueWith( + files => 1 + files.Result.Max( + file => + { + unsafe + { + using var pin = file.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Length); + return fdt.MaxTextureIndex; + } + }))); + this.prebakedTextureWraps = channelCountsTask.ToImmutableDictionary( + x => x.Key, + x => x.Value.ContinueWith(y => new IDalamudTextureWrap?[y.Result])); + this.texFiles = channelCountsTask.ToImmutableDictionary( + x => x.Key, + x => x.Value.ContinueWith( + y => Enumerable + .Range(1, 1 + ((y.Result - 1) / 4)) + .Select(z => Task.Run(() => dataManager.GetFile(string.Format(x.Key, z))!)) + .ToArray())); + this.defaultGlyphRanges = + this.fdtFiles[GameFontFamilyAndSize.Axis12] + .ContinueWith( + file => + { + unsafe + { + using var pin = file.Result.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Result.Length); + return fdt.ToGlyphRanges(); + } + }); + } + + /// + /// Gets or sets a value indicating whether to override configuration for UseAxis. + /// + public bool? UseAxisOverride { get; set; } = null; + + /// + /// Gets a value indicating whether to use AXIS fonts. + /// + public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; + + /// + /// Gets the service instance of . + /// + public Framework Framework { get; } + + /// + /// Gets the service instance of .
+ /// may not yet be available. + ///
+ public InterfaceManager InterfaceManager { get; } + + /// + /// Gets the async task for inside . + /// + public Task SceneTask { get; } + + /// + /// Gets the default glyph ranges (glyph ranges of ). + /// + public ushort[] DefaultGlyphRanges => ExtractResult(this.defaultGlyphRanges); + + /// + /// Gets a value indicating whether game symbol font file is available. + /// + public bool HasGameSymbolsFontFile => + this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol); + + /// + public void Dispose() + { + this.cancellationTokenSource.Cancel(); + this.scopedFinalizer.Dispose(); + this.cancellationTokenSource.Dispose(); + } + + /// + /// Creates a new instance of a class that implements the interface. + /// + /// Name of atlas, for debugging and logging purposes. + /// Specify how to auto rebuild. + /// Whether the fonts in the atlas is global scaled. + /// The new font atlas. + public IFontAtlas CreateFontAtlas( + string atlasName, + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled = true) => + new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled); + + /// + /// Adds the font from Dalamud Assets. + /// + /// The toolkitPostBuild. + /// The font. + /// The font config. + /// The address and size. + public ImFontPtr AddFont( + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + DalamudAsset asset, + in SafeFontConfig fontConfig) => + toolkitPreBuild.AddFontFromStream( + this.dalamudAssetManager.CreateStream(asset), + fontConfig, + false, + $"Asset({asset})"); + + /// + /// Gets the for the . + /// + /// The font family and size. + /// The . + public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); + + /// + public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) + { + var arr = ExtractResult(this.fdtFiles[gffas]); + var handle = arr.AsMemory().Pin(); + try + { + fdtFileView = new(handle.Pointer, arr.Length); + return handle; + } + catch + { + handle.Dispose(); + throw; + } + } + + /// + public int GetFontTextureCount(string texPathFormat) => + ExtractResult(this.prebakedTextureWraps[texPathFormat]).Length; + + /// + public TexFile GetTexFile(string texPathFormat, int index) => + ExtractResult(ExtractResult(this.texFiles[texPathFormat])[index]); + + /// + public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex) + { + lock (this.prebakedTextureWraps[texPathFormat]) + { + var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]); + var fileIndex = textureIndex / 4; + var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4]; + wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex); + return CloneTextureWrap(wraps[textureIndex]); + } + } + + private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); + + private static unsafe void ExtractChannelFromB8G8R8A8( + Span target, + ReadOnlySpan source, + int channelIndex, + bool targetIsB4G4R4A4) + { + var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); + + fixed (byte* sourcePtrImmutable = source) + { + var rptr = sourcePtrImmutable + channelIndex; + fixed (void* targetPtr = target) + { + if (targetIsB4G4R4A4) + { + var wptr = (ushort*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (ushort)((*rptr << 8) | 0x0FFF); + wptr++; + rptr += 4; + } + } + else + { + var wptr = (uint*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (uint)((*rptr << 24) | 0x00FFFFFF); + wptr++; + rptr += 4; + } + } + } + } + } + + /// + /// Clones a texture wrap, by getting a new reference to the underlying and the + /// texture behind. + /// + /// The to clone from. + /// The cloned . + private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) + { + var srv = CppObject.FromPointer(wrap.ImGuiHandle); + using var res = srv.Resource; + using var tex2D = res.QueryInterface(); + var description = tex2D.Description; + return new DalamudTextureWrap( + new D3DTextureWrap( + srv.QueryInterface(), + description.Width, + description.Height)); + } + + private static unsafe void ExtractChannelFromB4G4R4A4( + Span target, + ReadOnlySpan source, + int channelIndex, + bool targetIsB4G4R4A4) + { + var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); + fixed (byte* sourcePtrImmutable = source) + { + var rptr = sourcePtrImmutable + (channelIndex / 2); + var rshift = (channelIndex & 1) == 0 ? 0 : 4; + fixed (void* targetPtr = target) + { + if (targetIsB4G4R4A4) + { + var wptr = (ushort*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (ushort)(((*rptr >> rshift) << 12) | 0x0FFF); + wptr++; + rptr += 2; + } + } + else + { + var wptr = (uint*)targetPtr; + while (numPixels-- > 0) + { + var v = (*rptr >> rshift) & 0xF; + v |= v << 4; + *wptr = (uint)((v << 24) | 0x00FFFFFF); + wptr++; + rptr += 4; + } + } + } + } + } + + private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) + { + var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); + var numPixels = texFile.Header.Width * texFile.Header.Height; + + _ = Service.Get(); + var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var bpp = targetIsB4G4R4A4 ? 2 : 4; + var buffer = ArrayPool.Shared.Rent(numPixels * bpp); + try + { + var sliceSpan = texFile.SliceSpan(0, 0, out _, out _, out _); + switch (texFile.Header.Format) + { + case TexFile.TextureFormat.B4G4R4A4: + // Game ships with this format. + ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); + break; + case TexFile.TextureFormat.B8G8R8A8: + // In case of modded font textures. + ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); + break; + default: + // Unlikely. + ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4); + break; + } + + return this.scopedFinalizer.Add( + this.InterfaceManager.LoadImageFromDxgiFormat( + buffer, + texFile.Header.Width * bpp, + texFile.Header.Width, + texFile.Header.Height, + targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs new file mode 100644 index 000000000..99c817a91 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -0,0 +1,857 @@ +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Disposables; + +using Dalamud.Game.Text; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; + +using ImGuiNET; + +using Lumina.Data.Files; + +using Vector4 = System.Numerics.Vector4; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// A font handle that uses the game's built-in fonts, optionally with some styling. +/// +internal class GamePrebakedFontHandle : IFontHandle.IInternal +{ + /// + /// The smallest value of . + /// + public static readonly char SeIconCharMin = (char)Enum.GetValues().Min(); + + /// + /// The largest value of . + /// + public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); + + private IFontHandleManager? manager; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Font to use. + public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style) + { + if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined) + throw new ArgumentOutOfRangeException(nameof(style), style, null); + + if (style.SizePt <= 0) + throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style)); + + this.manager = manager; + this.FontStyle = style; + } + + /// + /// Provider for for `common/font/fontNN.tex`. + /// + public interface IGameFontTextureProvider + { + /// + /// Creates the for the .
+ /// Dispose after use. + ///
+ /// The font family and size. + /// The view. + /// Dispose this after use.. + public MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView); + + /// + /// Gets the number of font textures. + /// + /// Format of .tex path. + /// The number of textures. + public int GetFontTextureCount(string texPathFormat); + + /// + /// Gets the for the given index of a font. + /// + /// Format of .tex path. + /// The index of .tex file. + /// The . + public TexFile GetTexFile(string texPathFormat, int index); + + /// + /// Gets a new reference of the font texture. + /// + /// Format of .tex path. + /// Texture index. + /// The texture. + public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex); + } + + /// + /// Gets the font style. + /// + public GameFontStyle FontStyle { get; } + + /// + public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); + + /// + public bool Available => this.ImFont.IsNotNullAndLoaded(); + + /// + public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; + + private IFontHandleManager ManagerNotDisposed => + this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); + + /// + public void Dispose() + { + this.manager?.FreeFontHandle(this); + this.manager = null; + } + + /// + public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + + /// + /// Manager for s. + /// + internal sealed class HandleManager : IFontHandleManager + { + private readonly Dictionary gameFontsRc = new(); + private readonly object syncRoot = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the owner atlas. + /// An instance of . + public HandleManager(string atlasName, IGameFontTextureProvider gameFontTextureProvider) + { + this.GameFontTextureProvider = gameFontTextureProvider; + this.Name = $"{atlasName}:{nameof(GamePrebakedFontHandle)}:Manager"; + } + + /// + public event Action? RebuildRecommend; + + /// + public string Name { get; } + + /// + public IFontHandleSubstance? Substance { get; set; } + + /// + /// Gets an instance of . + /// + public IGameFontTextureProvider GameFontTextureProvider { get; } + + /// + public void Dispose() + { + this.Substance?.Dispose(); + this.Substance = null; + } + + /// + public IFontHandle NewFontHandle(GameFontStyle style) + { + var handle = new GamePrebakedFontHandle(this, style); + bool suggestRebuild; + lock (this.syncRoot) + { + this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; + suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; + } + + if (suggestRebuild) + this.RebuildRecommend?.Invoke(); + + return handle; + } + + /// + public void FreeFontHandle(IFontHandle handle) + { + if (handle is not GamePrebakedFontHandle ggfh) + return; + + lock (this.syncRoot) + { + if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) + return; + + if ((this.gameFontsRc[ggfh.FontStyle] -= 1) == 0) + this.gameFontsRc.Remove(ggfh.FontStyle); + } + } + + /// + public IFontHandleSubstance NewSubstance() + { + lock (this.syncRoot) + return new HandleSubstance(this, this.gameFontsRc.Keys); + } + } + + /// + /// Substance from . + /// + internal sealed class HandleSubstance : IFontHandleSubstance + { + private readonly HandleManager handleManager; + private readonly HashSet gameFontStyles; + + // Owned by this class, but ImFontPtr values still do not belong to this. + private readonly Dictionary fonts = new(); + private readonly Dictionary buildExceptions = new(); + private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new(); + + private readonly HashSet templatedFonts = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The manager. + /// The game font styles. + public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) + { + this.handleManager = manager; + Service.Get(); + this.gameFontStyles = new(gameFontStyles); + } + + /// + public IFontHandleManager Manager => this.handleManager; + + /// + public void Dispose() + { + } + + /// + /// Attaches game symbols to the given font. If font is null, it will be created. + /// + /// The toolkitPostBuild. + /// The font to attach to. + /// The game font style. + /// The intended glyph ranges. + /// if it is not empty; otherwise a new font. + public ImFontPtr AttachGameGlyphs( + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + ImFontPtr font, + GameFontStyle style, + ushort[]? glyphRanges = null) + { + if (font.IsNull()) + font = this.CreateTemplateFont(toolkitPreBuild, style.SizePx); + this.attachments.Add((font, style, glyphRanges)); + return font; + } + + /// + /// Creates or gets a relevant for the given . + /// + /// The game font style. + /// The toolkitPostBuild. + /// The font. + public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + try + { + if (!this.fonts.TryGetValue(style, out var plan)) + { + plan = new( + style, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + this.fonts[style] = plan; + } + + plan.AttachFont(plan.FullRangeFont); + return plan.FullRangeFont; + } + catch (Exception e) + { + this.buildExceptions[style] = e; + throw; + } + } + + /// + public ImFontPtr GetFontPtr(IFontHandle handle) => + handle is GamePrebakedFontHandle ggfh + ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default + : default; + + /// + public Exception? GetBuildException(IFontHandle handle) => + handle is GamePrebakedFontHandle ggfh ? this.buildExceptions.GetValueOrDefault(ggfh.FontStyle) : default; + + /// + public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + foreach (var style in this.gameFontStyles) + { + if (this.fonts.ContainsKey(style)) + continue; + + try + { + _ = this.GetOrCreateFont(style, toolkitPreBuild); + } + catch + { + // ignore; it should have been recorded from the call + } + } + } + + /// + public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + foreach (var (font, style, ranges) in this.attachments) + { + var effectiveStyle = + toolkitPreBuild.IsGlobalScaleIgnored(font) + ? style.Scale(1 / toolkitPreBuild.Scale) + : style; + if (!this.fonts.TryGetValue(style, out var plan)) + { + plan = new( + effectiveStyle, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + this.fonts[style] = plan; + } + + plan.AttachFont(font, ranges); + } + + foreach (var plan in this.fonts.Values) + { + plan.EnsureGlyphs(toolkitPreBuild.NewImAtlas); + } + } + + /// + public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + var allTextureIndices = new Dictionary(); + var allTexFiles = new Dictionary(); + using var rentReturn = Disposable.Create( + () => + { + foreach (var x in allTextureIndices.Values) + ArrayPool.Shared.Return(x); + foreach (var x in allTexFiles.Values) + ArrayPool.Shared.Return(x); + }); + + var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; + var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; + for (var i = 0; i < pixels8Array.Length; i++) + toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out _); + + foreach (var (style, plan) in this.fonts) + { + try + { + foreach (var font in plan.Ranges.Keys) + this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale); + + plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths); + plan.CopyGlyphsToRanges(toolkitPostBuild); + plan.PostProcessFullRangeFont(toolkitPostBuild.Scale); + } + catch (Exception e) + { + this.buildExceptions[style] = e; + this.fonts[style] = default; + } + } + } + + /// + public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) + { + // Irrelevant + } + + /// + /// Creates a new template font. + /// + /// The toolkitPostBuild. + /// The size of the font. + /// The font. + private ImFontPtr CreateTemplateFont(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, float sizePx) + { + var font = toolkitPreBuild.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() + { + GlyphRanges = new ushort[] { ' ', ' ', '\0' }, + SizePx = sizePx, + }); + this.templatedFonts.Add(font); + return font; + } + + private unsafe void PatchFontMetricsIfNecessary(GameFontStyle style, ImFontPtr font, float atlasScale) + { + if (!this.templatedFonts.Contains(font)) + return; + + var fas = style.Scale(atlasScale).FamilyAndSize; + using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); + ref var fdtFontHeader = ref fdt.FontHeader; + var fontPtr = font.NativePtr; + + var scale = style.SizePt / fdtFontHeader.Size; + fontPtr->Ascent = fdtFontHeader.Ascent * scale; + fontPtr->Descent = fdtFontHeader.Descent * scale; + fontPtr->EllipsisChar = '…'; + } + } + + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "Internal")] + private sealed class FontDrawPlan : IDisposable + { + public readonly GameFontStyle Style; + public readonly GameFontStyle BaseStyle; + public readonly GameFontFamilyAndSizeAttribute BaseAttr; + public readonly int TexCount; + public readonly Dictionary Ranges = new(); + public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new(); + public readonly ushort[] RectLookup = new ushort[0x10000]; + public readonly FdtFileView Fdt; + public readonly ImFontPtr FullRangeFont; + + private readonly IDisposable fdtHandle; + private readonly IGameFontTextureProvider gftp; + + public FontDrawPlan( + GameFontStyle style, + float scale, + IGameFontTextureProvider gameFontTextureProvider, + ImFontPtr fullRangeFont) + { + this.Style = style; + this.BaseStyle = style.Scale(scale); + this.BaseAttr = this.BaseStyle.FamilyAndSize.GetAttribute()!; + this.gftp = gameFontTextureProvider; + this.TexCount = this.gftp.GetFontTextureCount(this.BaseAttr.TexPathFormat); + this.fdtHandle = this.gftp.CreateFdtFileView(this.BaseStyle.FamilyAndSize, out this.Fdt); + this.RectLookup.AsSpan().Fill(ushort.MaxValue); + this.FullRangeFont = fullRangeFont; + this.Ranges[fullRangeFont] = new(0x10000); + } + + public void Dispose() + { + this.fdtHandle.Dispose(); + } + + public void AttachFont(ImFontPtr font, ushort[]? glyphRanges = null) + { + if (!this.Ranges.TryGetValue(font, out var rangeBitArray)) + rangeBitArray = this.Ranges[font] = new(0x10000); + + if (glyphRanges is null) + { + foreach (ref var g in this.Fdt.Glyphs) + { + var c = g.CharInt; + if (c is >= 0x20 and <= 0xFFFE) + rangeBitArray[c] = true; + } + + return; + } + + for (var i = 0; i < glyphRanges.Length - 1; i += 2) + { + if (glyphRanges[i] == 0) + break; + var from = (int)glyphRanges[i]; + var to = (int)glyphRanges[i + 1]; + for (var j = from; j <= to; j++) + rangeBitArray[j] = true; + } + } + + public unsafe void EnsureGlyphs(ImFontAtlasPtr atlas) + { + var glyphs = this.Fdt.Glyphs; + var ranges = this.Ranges[this.FullRangeFont]; + foreach (var (font, extraRange) in this.Ranges) + { + if (font.NativePtr != this.FullRangeFont.NativePtr) + ranges.Or(extraRange); + } + + if (this.Style is not { Weight: 0, SkewStrength: 0 }) + { + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint > char.MaxValue) + continue; + if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) + continue; + + var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(this.Fdt.FontHeader, glyph); + this.RectLookup[cint] = (ushort)this.Rects.Count; + this.Rects.Add( + ( + atlas.AddCustomRectFontGlyph( + this.FullRangeFont, + (char)cint, + glyph.BoundingWidth + widthAdjustment, + glyph.BoundingHeight, + glyph.AdvanceWidth, + new(this.BaseAttr.HorizontalOffset, glyph.CurrentOffsetY)), + fdtGlyphIndex)); + } + } + else + { + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint > char.MaxValue) + continue; + if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) + continue; + + this.RectLookup[cint] = (ushort)this.Rects.Count; + this.Rects.Add((-1, fdtGlyphIndex)); + } + } + } + + public unsafe void PostProcessFullRangeFont(float atlasScale) + { + var round = 1 / atlasScale; + var pfrf = this.FullRangeFont.NativePtr; + ref var frf = ref *pfrf; + + frf.FontSize = MathF.Round(frf.FontSize / round) * round; + frf.Ascent = MathF.Round(frf.Ascent / round) * round; + frf.Descent = MathF.Round(frf.Descent / round) * round; + + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; + foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan) + { + var w = (g.X1 - g.X0) * scale; + var h = (g.Y1 - g.Y0) * scale; + g.X0 = MathF.Round((g.X0 * scale) / round) * round; + g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; + g.X1 = g.X0 + w; + g.Y1 = g.Y0 + h; + g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; + } + + var fullRange = this.Ranges[this.FullRangeFont]; + foreach (ref var k in this.Fdt.PairAdjustments) + { + var (leftInt, rightInt) = (k.LeftInt, k.RightInt); + if (leftInt > char.MaxValue || rightInt > char.MaxValue) + continue; + if (!fullRange[leftInt] || !fullRange[rightInt]) + continue; + ImGuiNative.ImFont_AddKerningPair( + pfrf, + (ushort)leftInt, + (ushort)rightInt, + MathF.Round((k.RightOffset * scale) / round) * round); + } + + pfrf->FallbackGlyph = null; + ImGuiNative.ImFont_BuildLookupTable(pfrf); + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = ImGuiNative.ImFont_FindGlyphNoFallback(pfrf, fallbackCharCandidate); + if ((nint)glyph == IntPtr.Zero) + continue; + frf.FallbackChar = fallbackCharCandidate; + frf.FallbackGlyph = glyph; + frf.FallbackHotData = + (ImFontGlyphHotData*)frf.IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + + public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; + var atlasScale = toolkitPostBuild.Scale; + var round = 1 / atlasScale; + + foreach (var (font, rangeBits) in this.Ranges) + { + if (font.NativePtr == this.FullRangeFont.NativePtr) + continue; + + var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font); + + var lookup = font.IndexLookupWrapped(); + var glyphs = font.GlyphsWrapped(); + foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan) + { + if (!rangeBits[sourceGlyph.Codepoint]) + continue; + + var glyphIndex = ushort.MaxValue; + if (sourceGlyph.Codepoint < lookup.Length) + glyphIndex = lookup[sourceGlyph.Codepoint]; + + if (glyphIndex == ushort.MaxValue) + { + glyphIndex = (ushort)glyphs.Length; + glyphs.Add(default); + } + + ref var g = ref glyphs[glyphIndex]; + g = sourceGlyph; + if (noGlobalScale) + { + g.XY *= scale; + g.AdvanceX *= scale; + } + else + { + var w = (g.X1 - g.X0) * scale; + var h = (g.Y1 - g.Y0) * scale; + g.X0 = MathF.Round((g.X0 * scale) / round) * round; + g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; + g.X1 = g.X0 + w; + g.Y1 = g.Y0 + h; + g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; + } + } + + foreach (ref var k in this.Fdt.PairAdjustments) + { + var (leftInt, rightInt) = (k.LeftInt, k.RightInt); + if (leftInt > char.MaxValue || rightInt > char.MaxValue) + continue; + if (!rangeBits[leftInt] || !rangeBits[rightInt]) + continue; + if (noGlobalScale) + { + font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale); + } + else + { + font.AddKerningPair( + (ushort)leftInt, + (ushort)rightInt, + MathF.Round((k.RightOffset * scale) / round) * round); + } + } + + font.NativePtr->FallbackGlyph = null; + font.BuildLookupTable(); + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate).NativePtr; + if ((nint)glyph == IntPtr.Zero) + continue; + + ref var frf = ref *font.NativePtr; + frf.FallbackChar = fallbackCharCandidate; + frf.FallbackGlyph = glyph; + frf.FallbackHotData = + (ImFontGlyphHotData*)frf.IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + } + + public unsafe void SetFullRangeFontGlyphs( + IFontAtlasBuildToolkitPostBuild toolkitPostBuild, + Dictionary allTexFiles, + Dictionary allTextureIndices, + byte*[] pixels8Array, + int[] widths) + { + var glyphs = this.FullRangeFont.GlyphsWrapped(); + var lookups = this.FullRangeFont.IndexLookupWrapped(); + + ref var fdtFontHeader = ref this.Fdt.FontHeader; + var fdtGlyphs = this.Fdt.Glyphs; + var fdtTexSize = new Vector4( + this.Fdt.FontHeader.TextureWidth, + this.Fdt.FontHeader.TextureHeight, + this.Fdt.FontHeader.TextureWidth, + this.Fdt.FontHeader.TextureHeight); + + if (!allTexFiles.TryGetValue(this.BaseAttr.TexPathFormat, out var texFiles)) + { + allTexFiles.Add( + this.BaseAttr.TexPathFormat, + texFiles = ArrayPool.Shared.Rent(this.TexCount)); + } + + if (!allTextureIndices.TryGetValue(this.BaseAttr.TexPathFormat, out var textureIndices)) + { + allTextureIndices.Add( + this.BaseAttr.TexPathFormat, + textureIndices = ArrayPool.Shared.Rent(this.TexCount)); + textureIndices.AsSpan(0, this.TexCount).Fill(-1); + } + + var pixelWidth = Math.Max(1, (int)MathF.Ceiling(this.BaseStyle.Weight + 1)); + var pixelStrength = stackalloc byte[pixelWidth]; + for (var i = 0; i < pixelWidth; i++) + pixelStrength[i] = (byte)(255 * Math.Min(1f, (this.BaseStyle.Weight + 1) - i)); + + var minGlyphY = 0; + var maxGlyphY = 0; + foreach (ref var g in fdtGlyphs) + { + minGlyphY = Math.Min(g.CurrentOffsetY, minGlyphY); + maxGlyphY = Math.Max(g.BoundingHeight + g.CurrentOffsetY, maxGlyphY); + } + + var horzShift = stackalloc int[maxGlyphY - minGlyphY]; + var horzBlend = stackalloc byte[maxGlyphY - minGlyphY]; + horzShift -= minGlyphY; + horzBlend -= minGlyphY; + if (this.BaseStyle.BaseSkewStrength != 0) + { + for (var i = minGlyphY; i < maxGlyphY; i++) + { + float blend = this.BaseStyle.BaseSkewStrength switch + { + > 0 => fdtFontHeader.LineHeight - i, + < 0 => -i, + _ => throw new InvalidOperationException(), + }; + blend *= this.BaseStyle.BaseSkewStrength / fdtFontHeader.LineHeight; + horzShift[i] = (int)MathF.Floor(blend); + horzBlend[i] = (byte)(255 * (blend - horzShift[i])); + } + } + + foreach (var (rectId, fdtGlyphIndex) in this.Rects) + { + ref var fdtGlyph = ref fdtGlyphs[fdtGlyphIndex]; + if (rectId == -1) + { + ref var textureIndex = ref textureIndices[fdtGlyph.TextureIndex]; + if (textureIndex == -1) + { + textureIndex = toolkitPostBuild.StoreTexture( + this.gftp.NewFontTextureRef(this.BaseAttr.TexPathFormat, fdtGlyph.TextureIndex), + true); + } + + var glyph = new ImGuiHelpers.ImFontGlyphReal + { + AdvanceX = fdtGlyph.AdvanceWidth, + Codepoint = fdtGlyph.Char, + Colored = false, + TextureIndex = textureIndex, + Visible = true, + X0 = this.BaseAttr.HorizontalOffset, + Y0 = fdtGlyph.CurrentOffsetY, + U0 = fdtGlyph.TextureOffsetX, + V0 = fdtGlyph.TextureOffsetY, + U1 = fdtGlyph.BoundingWidth, + V1 = fdtGlyph.BoundingHeight, + }; + + glyph.XY1 = glyph.XY0 + glyph.UV1; + glyph.UV1 += glyph.UV0; + glyph.UV /= fdtTexSize; + + glyphs.Add(glyph); + } + else + { + ref var rc = ref *(ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas + .GetCustomRectByIndex(rectId) + .NativePtr; + var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(fdtFontHeader, fdtGlyph); + + // Glyph is scaled at this point; undo that. + ref var glyph = ref glyphs[lookups[rc.GlyphId]]; + glyph.X0 = this.BaseAttr.HorizontalOffset; + glyph.Y0 = fdtGlyph.CurrentOffsetY; + glyph.X1 = glyph.X0 + fdtGlyph.BoundingWidth + widthAdjustment; + glyph.Y1 = glyph.Y0 + fdtGlyph.BoundingHeight; + glyph.AdvanceX = fdtGlyph.AdvanceWidth; + + var pixels8 = pixels8Array[rc.TextureIndex]; + var width = widths[rc.TextureIndex]; + texFiles[fdtGlyph.TextureFileIndex] ??= + this.gftp.GetTexFile(this.BaseAttr.TexPathFormat, fdtGlyph.TextureFileIndex); + var sourceBuffer = texFiles[fdtGlyph.TextureFileIndex].ImageData; + var sourceBufferDelta = fdtGlyph.TextureChannelByteIndex; + + for (var y = 0; y < fdtGlyph.BoundingHeight; y++) + { + var sourcePixelIndex = + ((fdtGlyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + fdtGlyph.TextureOffsetX; + sourcePixelIndex *= 4; + sourcePixelIndex += sourceBufferDelta; + var blend1 = horzBlend[fdtGlyph.CurrentOffsetY + y]; + + var targetOffset = ((rc.Y + y) * width) + rc.X; + for (var x = 0; x < rc.Width; x++) + pixels8[targetOffset + x] = 0; + + targetOffset += horzShift[fdtGlyph.CurrentOffsetY + y]; + if (blend1 == 0) + { + for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) + { + var n = sourceBuffer[sourcePixelIndex + 4]; + for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) + { + ref var p = ref pixels8[targetOffset + boldOffset]; + p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255)); + } + } + } + else + { + var blend2 = 255 - blend1; + for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) + { + var a1 = sourceBuffer[sourcePixelIndex]; + var a2 = x == fdtGlyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourcePixelIndex + 4]; + var n = (a1 * blend1) + (a2 * blend2); + + for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) + { + ref var p = ref pixels8[targetOffset + boldOffset]; + p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255 / 255)); + } + } + } + } + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs new file mode 100644 index 000000000..93c688608 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -0,0 +1,32 @@ +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Manager for . +/// +internal interface IFontHandleManager : IDisposable +{ + /// + event Action? RebuildRecommend; + + /// + /// Gets the name of the font handle manager. For logging and debugging purposes. + /// + string Name { get; } + + /// + /// Gets or sets the active font handle substance. + /// + IFontHandleSubstance? Substance { get; set; } + + /// + /// Decrease font reference counter. + /// + /// Handle being released. + void FreeFontHandle(IFontHandle handle); + + /// + /// Creates a new substance of the font atlas. + /// + /// The new substance. + IFontHandleSubstance NewSubstance(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs new file mode 100644 index 000000000..f6c5c6591 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -0,0 +1,54 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Substance of a font. +/// +internal interface IFontHandleSubstance : IDisposable +{ + /// + /// Gets the manager relevant to this instance of . + /// + IFontHandleManager Manager { get; } + + /// + /// Gets the font. + /// + /// The handle to get from. + /// Corresponding font or null. + ImFontPtr GetFontPtr(IFontHandle handle); + + /// + /// Gets the exception happened while loading for the font. + /// + /// The handle to get from. + /// Corresponding font or null. + Exception? GetBuildException(IFontHandle handle); + + /// + /// Called before call. + /// + /// The toolkit. + void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); + + /// + /// Called between and calls.
+ /// Any further modification to will result in undefined behavior. + ///
+ /// The toolkit. + void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); + + /// + /// Called after call. + /// + /// The toolkit. + void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild); + + /// + /// Called on the specific thread depending on after + /// promoting the staging atlas to direct use with . + /// + /// The toolkit. + void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs new file mode 100644 index 000000000..8e7149853 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs @@ -0,0 +1,203 @@ +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + private struct Fixed : IComparable + { + public ushort Major; + public ushort Minor; + + public Fixed(ushort major, ushort minor) + { + this.Major = major; + this.Minor = minor; + } + + public Fixed(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Major); + span.ReadBig(ref offset, out this.Minor); + } + + public int CompareTo(Fixed other) + { + var majorComparison = this.Major.CompareTo(other.Major); + return majorComparison != 0 ? majorComparison : this.Minor.CompareTo(other.Minor); + } + } + + private struct KerningPair : IEquatable + { + public ushort Left; + public ushort Right; + public short Value; + + public KerningPair(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Left); + span.ReadBig(ref offset, out this.Right); + span.ReadBig(ref offset, out this.Value); + } + + public KerningPair(ushort left, ushort right, short value) + { + this.Left = left; + this.Right = right; + this.Value = value; + } + + public static bool operator ==(KerningPair left, KerningPair right) => left.Equals(right); + + public static bool operator !=(KerningPair left, KerningPair right) => !left.Equals(right); + + public static KerningPair ReverseEndianness(KerningPair pair) => new() + { + Left = BinaryPrimitives.ReverseEndianness(pair.Left), + Right = BinaryPrimitives.ReverseEndianness(pair.Right), + Value = BinaryPrimitives.ReverseEndianness(pair.Value), + }; + + public bool Equals(KerningPair other) => + this.Left == other.Left && this.Right == other.Right && this.Value == other.Value; + + public override bool Equals(object? obj) => obj is KerningPair other && this.Equals(other); + + public override int GetHashCode() => HashCode.Combine(this.Left, this.Right, this.Value); + + public override string ToString() => $"KerningPair[{this.Left}, {this.Right}] = {this.Value}"; + } + + [StructLayout(LayoutKind.Explicit, Size = 4)] + private struct PlatformAndEncoding + { + [FieldOffset(0)] + public PlatformId Platform; + + [FieldOffset(2)] + public UnicodeEncodingId UnicodeEncoding; + + [FieldOffset(2)] + public MacintoshEncodingId MacintoshEncoding; + + [FieldOffset(2)] + public IsoEncodingId IsoEncoding; + + [FieldOffset(2)] + public WindowsEncodingId WindowsEncoding; + + public PlatformAndEncoding(PointerSpan source) + { + var offset = 0; + source.ReadBig(ref offset, out this.Platform); + source.ReadBig(ref offset, out this.UnicodeEncoding); + } + + public static PlatformAndEncoding ReverseEndianness(PlatformAndEncoding value) => new() + { + Platform = (PlatformId)BinaryPrimitives.ReverseEndianness((ushort)value.Platform), + UnicodeEncoding = (UnicodeEncodingId)BinaryPrimitives.ReverseEndianness((ushort)value.UnicodeEncoding), + }; + + public readonly string Decode(Span data) + { + switch (this.Platform) + { + case PlatformId.Unicode: + switch (this.UnicodeEncoding) + { + case UnicodeEncodingId.Unicode_2_0_Bmp: + case UnicodeEncodingId.Unicode_2_0_Full: + return Encoding.BigEndianUnicode.GetString(data); + } + + break; + + case PlatformId.Macintosh: + switch (this.MacintoshEncoding) + { + case MacintoshEncodingId.Roman: + return Encoding.ASCII.GetString(data); + } + + break; + + case PlatformId.Windows: + switch (this.WindowsEncoding) + { + case WindowsEncodingId.Symbol: + case WindowsEncodingId.UnicodeBmp: + case WindowsEncodingId.UnicodeFullRepertoire: + return Encoding.BigEndianUnicode.GetString(data); + } + + break; + } + + throw new NotSupportedException(); + } + } + + [StructLayout(LayoutKind.Explicit)] + private struct TagStruct : IEquatable, IComparable + { + [FieldOffset(0)] + public unsafe fixed byte Tag[4]; + + [FieldOffset(0)] + public uint NativeValue; + + public unsafe TagStruct(char c1, char c2, char c3, char c4) + { + this.Tag[0] = checked((byte)c1); + this.Tag[1] = checked((byte)c2); + this.Tag[2] = checked((byte)c3); + this.Tag[3] = checked((byte)c4); + } + + public unsafe TagStruct(PointerSpan span) + { + this.Tag[0] = span[0]; + this.Tag[1] = span[1]; + this.Tag[2] = span[2]; + this.Tag[3] = span[3]; + } + + public unsafe TagStruct(ReadOnlySpan span) + { + this.Tag[0] = span[0]; + this.Tag[1] = span[1]; + this.Tag[2] = span[2]; + this.Tag[3] = span[3]; + } + + public unsafe byte this[int index] + { + get => this.Tag[index]; + set => this.Tag[index] = value; + } + + public static bool operator ==(TagStruct left, TagStruct right) => left.Equals(right); + + public static bool operator !=(TagStruct left, TagStruct right) => !left.Equals(right); + + public bool Equals(TagStruct other) => this.NativeValue == other.NativeValue; + + public override bool Equals(object? obj) => obj is TagStruct other && this.Equals(other); + + public override int GetHashCode() => (int)this.NativeValue; + + public int CompareTo(TagStruct other) => this.NativeValue.CompareTo(other.NativeValue); + + public override unsafe string ToString() => + $"0x{this.NativeValue:08X} \"{(char)this.Tag[0]}{(char)this.Tag[1]}{(char)this.Tag[2]}{(char)this.Tag[3]}\""; + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs new file mode 100644 index 000000000..f6a653a51 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] + private enum IsoEncodingId : ushort + { + Ascii = 0, + Iso_10646 = 1, + Iso_8859_1 = 2, + } + + private enum MacintoshEncodingId : ushort + { + Roman = 0, + } + + private enum NameId : ushort + { + CopyrightNotice = 0, + FamilyName = 1, + SubfamilyName = 2, + UniqueId = 3, + FullFontName = 4, + VersionString = 5, + PostScriptName = 6, + Trademark = 7, + Manufacturer = 8, + Designer = 9, + Description = 10, + UrlVendor = 11, + UrlDesigner = 12, + LicenseDescription = 13, + LicenseInfoUrl = 14, + TypographicFamilyName = 16, + TypographicSubfamilyName = 17, + CompatibleFullMac = 18, + SampleText = 19, + PoscSriptCidFindFontName = 20, + WwsFamilyName = 21, + WwsSubfamilyName = 22, + LightBackgroundPalette = 23, + DarkBackgroundPalette = 24, + VariationPostScriptNamePrefix = 25, + } + + private enum PlatformId : ushort + { + Unicode = 0, + Macintosh = 1, // discouraged + Iso = 2, // deprecated + Windows = 3, + Custom = 4, // OTF Windows NT compatibility mapping + } + + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] + private enum UnicodeEncodingId : ushort + { + Unicode_1_0 = 0, // deprecated + Unicode_1_1 = 1, // deprecated + IsoIec_10646 = 2, // deprecated + Unicode_2_0_Bmp = 3, + Unicode_2_0_Full = 4, + UnicodeVariationSequences = 5, + UnicodeFullRepertoire = 6, + } + + private enum WindowsEncodingId : ushort + { + Symbol = 0, + UnicodeBmp = 1, + ShiftJis = 2, + Prc = 3, + Big5 = 4, + Wansung = 5, + Johab = 6, + UnicodeFullRepertoire = 10, + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs new file mode 100644 index 000000000..3d89dd806 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs @@ -0,0 +1,148 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] +[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] +[SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1310:Field names should not contain underscore", + Justification = "Version name")] +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name")] +internal static partial class TrueTypeUtils +{ + private readonly struct SfntFile : IReadOnlyDictionary> + { + // http://formats.kaitai.io/ttf/ttf.svg + + public static readonly TagStruct FileTagTrueType1 = new('1', '\0', '\0', '\0'); + public static readonly TagStruct FileTagType1 = new('t', 'y', 'p', '1'); + public static readonly TagStruct FileTagOpenTypeWithCff = new('O', 'T', 'T', 'O'); + public static readonly TagStruct FileTagOpenType1_0 = new('\0', '\x01', '\0', '\0'); + public static readonly TagStruct FileTagTrueTypeApple = new('t', 'r', 'u', 'e'); + + public readonly PointerSpan Memory; + public readonly int OffsetInCollection; + public readonly ushort TableCount; + + public SfntFile(PointerSpan memory, int offsetInCollection = 0) + { + var span = memory.Span; + this.Memory = memory; + this.OffsetInCollection = offsetInCollection; + this.TableCount = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); + } + + public int Count => this.TableCount; + + public IEnumerable Keys => this.Select(x => x.Key); + + public IEnumerable> Values => this.Select(x => x.Value); + + public PointerSpan this[TagStruct key] => this.First(x => x.Key == key).Value; + + public IEnumerator>> GetEnumerator() + { + var offset = 12; + for (var i = 0; i < this.TableCount; i++) + { + var dte = new DirectoryTableEntry(this.Memory[offset..]); + yield return new(dte.Tag, this.Memory.Slice(dte.Offset - this.OffsetInCollection, dte.Length)); + + offset += Unsafe.SizeOf(); + } + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public bool ContainsKey(TagStruct key) => this.Any(x => x.Key == key); + + public bool TryGetValue(TagStruct key, out PointerSpan value) + { + foreach (var (k, v) in this) + { + if (k == key) + { + value = v; + return true; + } + } + + value = default; + return false; + } + + public readonly struct DirectoryTableEntry + { + public readonly PointerSpan Memory; + + public DirectoryTableEntry(PointerSpan span) => this.Memory = span; + + public TagStruct Tag => new(this.Memory); + + public uint Checksum => this.Memory.ReadU32Big(4); + + public int Offset => this.Memory.ReadI32Big(8); + + public int Length => this.Memory.ReadI32Big(12); + } + } + + private readonly struct TtcFile : IReadOnlyList + { + public static readonly TagStruct FileTag = new('t', 't', 'c', 'f'); + + public readonly PointerSpan Memory; + public readonly TagStruct Tag; + public readonly ushort MajorVersion; + public readonly ushort MinorVersion; + public readonly int FontCount; + + public TtcFile(PointerSpan memory) + { + var span = memory.Span; + this.Memory = memory; + this.Tag = new(span); + if (this.Tag != FileTag) + throw new InvalidOperationException(); + + this.MajorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); + this.MinorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[6..]); + this.FontCount = BinaryPrimitives.ReadInt32BigEndian(span[8..]); + } + + public int Count => this.FontCount; + + public SfntFile this[int index] + { + get + { + if (index < 0 || index >= this.FontCount) + { + throw new IndexOutOfRangeException( + $"The requested font #{index} does not exist in this .ttc file."); + } + + var offset = BinaryPrimitives.ReadInt32BigEndian(this.Memory.Span[(12 + 4 * index)..]); + return new(this.Memory[offset..], offset); + } + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.FontCount; i++) + yield return this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs new file mode 100644 index 000000000..d200de47b --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs @@ -0,0 +1,259 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + [Flags] + private enum LookupFlags : byte + { + RightToLeft = 1 << 0, + IgnoreBaseGlyphs = 1 << 1, + IgnoreLigatures = 1 << 2, + IgnoreMarks = 1 << 3, + UseMarkFilteringSet = 1 << 4, + } + + private enum LookupType : ushort + { + SingleAdjustment = 1, + PairAdjustment = 2, + CursiveAttachment = 3, + MarkToBaseAttachment = 4, + MarkToLigatureAttachment = 5, + MarkToMarkAttachment = 6, + ContextPositioning = 7, + ChainedContextPositioning = 8, + ExtensionPositioning = 9, + } + + private readonly struct ClassDefTable + { + public readonly PointerSpan Memory; + + public ClassDefTable(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public Format1ClassArray Format1 => new(this.Memory); + + public Format2ClassRanges Format2 => new(this.Memory); + + public IEnumerable<(ushort Class, ushort GlyphId)> Enumerate() + { + switch (this.Format) + { + case 1: + { + var format1 = this.Format1; + var startId = format1.StartGlyphId; + var count = format1.GlyphCount; + var classes = format1.ClassValueArray; + for (var i = 0; i < count; i++) + yield return (classes[i], (ushort)(i + startId)); + + break; + } + + case 2: + { + foreach (var range in this.Format2.ClassValueArray) + { + var @class = range.Class; + var startId = range.StartGlyphId; + var count = range.EndGlyphId - startId + 1; + for (var i = 0; i < count; i++) + yield return (@class, (ushort)(startId + i)); + } + + break; + } + } + } + + [Pure] + public ushort GetClass(ushort glyphId) + { + switch (this.Format) + { + case 1: + { + var format1 = this.Format1; + var startId = format1.StartGlyphId; + if (startId <= glyphId && glyphId < startId + format1.GlyphCount) + return this.Format1.ClassValueArray[glyphId - startId]; + + break; + } + + case 2: + { + var rangeSpan = this.Format2.ClassValueArray; + var i = rangeSpan.BinarySearch(new Format2ClassRanges.ClassRangeRecord { EndGlyphId = glyphId }); + if (i >= 0 && rangeSpan[i].ContainsGlyph(glyphId)) + return rangeSpan[i].Class; + + break; + } + } + + return 0; + } + + public readonly struct Format1ClassArray + { + public readonly PointerSpan Memory; + + public Format1ClassArray(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort StartGlyphId => this.Memory.ReadU16Big(2); + + public ushort GlyphCount => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan ClassValueArray => new( + this.Memory[6..].As(this.GlyphCount), + BinaryPrimitives.ReverseEndianness); + } + + public readonly struct Format2ClassRanges + { + public readonly PointerSpan Memory; + + public Format2ClassRanges(PointerSpan memory) => this.Memory = memory; + + public ushort ClassRangeCount => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan ClassValueArray => new( + this.Memory[4..].As(this.ClassRangeCount), + ClassRangeRecord.ReverseEndianness); + + public struct ClassRangeRecord : IComparable + { + public ushort StartGlyphId; + public ushort EndGlyphId; + public ushort Class; + + public static ClassRangeRecord ReverseEndianness(ClassRangeRecord value) => new() + { + StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), + EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), + Class = BinaryPrimitives.ReverseEndianness(value.Class), + }; + + public int CompareTo(ClassRangeRecord other) => this.EndGlyphId.CompareTo(other.EndGlyphId); + + public bool ContainsGlyph(ushort glyphId) => + this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; + } + } + } + + private readonly struct CoverageTable + { + public readonly PointerSpan Memory; + + public CoverageTable(PointerSpan memory) => this.Memory = memory; + + public enum CoverageFormat : ushort + { + Glyphs = 1, + RangeRecords = 2, + } + + public CoverageFormat Format => this.Memory.ReadEnumBig(0); + + public ushort Count => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan Glyphs => + this.Format == CoverageFormat.Glyphs + ? new(this.Memory[4..].As(this.Count), BinaryPrimitives.ReverseEndianness) + : default(BigEndianPointerSpan); + + public BigEndianPointerSpan RangeRecords => + this.Format == CoverageFormat.RangeRecords + ? new(this.Memory[4..].As(this.Count), RangeRecord.ReverseEndianness) + : default(BigEndianPointerSpan); + + public int GetCoverageIndex(ushort glyphId) + { + switch (this.Format) + { + case CoverageFormat.Glyphs: + return this.Glyphs.BinarySearch(glyphId); + + case CoverageFormat.RangeRecords: + { + var index = this.RangeRecords.BinarySearch( + (in RangeRecord record) => glyphId.CompareTo(record.EndGlyphId)); + + if (index >= 0 && this.RangeRecords[index].ContainsGlyph(glyphId)) + return index; + + return -1; + } + + default: + return -1; + } + } + + public struct RangeRecord + { + public ushort StartGlyphId; + public ushort EndGlyphId; + public ushort StartCoverageIndex; + + public static RangeRecord ReverseEndianness(RangeRecord value) => new() + { + StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), + EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), + StartCoverageIndex = BinaryPrimitives.ReverseEndianness(value.StartCoverageIndex), + }; + + public bool ContainsGlyph(ushort glyphId) => + this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; + } + } + + private readonly struct LookupTable : IEnumerable> + { + public readonly PointerSpan Memory; + + public LookupTable(PointerSpan memory) => this.Memory = memory; + + public LookupType Type => this.Memory.ReadEnumBig(0); + + public byte MarkAttachmentType => this.Memory[2]; + + public LookupFlags Flags => (LookupFlags)this.Memory[3]; + + public ushort SubtableCount => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan SubtableOffsets => new( + this.Memory[6..].As(this.SubtableCount), + BinaryPrimitives.ReverseEndianness); + + public PointerSpan this[int index] => this.Memory[this.SubtableOffsets[this.EnsureIndex(index)] ..]; + + public IEnumerator> GetEnumerator() + { + foreach (var i in Enumerable.Range(0, this.SubtableCount)) + yield return this.Memory[this.SubtableOffsets[i] ..]; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => index >= 0 && index < this.SubtableCount + ? index + : throw new IndexOutOfRangeException(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs new file mode 100644 index 000000000..c91df4ff2 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs @@ -0,0 +1,443 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + private delegate int BinarySearchComparer(in T value); + + private static IDisposable CreatePointerSpan(this T[] data, out PointerSpan pointerSpan) + where T : unmanaged + { + var gchandle = GCHandle.Alloc(data, GCHandleType.Pinned); + pointerSpan = new(gchandle.AddrOfPinnedObject(), data.Length); + return Disposable.Create(() => gchandle.Free()); + } + + private static int BinarySearch(this IReadOnlyList span, in T value) + where T : unmanaged, IComparable + { + var l = 0; + var r = span.Count - 1; + while (l <= r) + { + var i = (int)(((uint)r + (uint)l) >> 1); + var c = value.CompareTo(span[i]); + switch (c) + { + case 0: + return i; + case > 0: + l = i + 1; + break; + default: + r = i - 1; + break; + } + } + + return ~l; + } + + private static int BinarySearch(this IReadOnlyList span, BinarySearchComparer comparer) + where T : unmanaged + { + var l = 0; + var r = span.Count - 1; + while (l <= r) + { + var i = (int)(((uint)r + (uint)l) >> 1); + var c = comparer(span[i]); + switch (c) + { + case 0: + return i; + case > 0: + l = i + 1; + break; + default: + r = i - 1; + break; + } + } + + return ~l; + } + + private static short ReadI16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); + + private static int ReadI32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); + + private static long ReadI64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); + + private static ushort ReadU16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); + + private static uint ReadU32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); + + private static ulong ReadU64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); + + private static Half ReadF16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); + + private static float ReadF32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); + + private static double ReadF64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out short value) => + value = BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out int value) => + value = BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out long value) => + value = BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out ushort value) => + value = BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out uint value) => + value = BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out ulong value) => + value = BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out Half value) => + value = BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out float value) => + value = BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out double value) => + value = BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, ref int offset, out short value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out int value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out long value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out ushort value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out uint value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out ulong value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out Half value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out float value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out double value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static unsafe T ReadEnumBig(this PointerSpan ps, int offset) where T : unmanaged, Enum + { + switch (Marshal.SizeOf(Enum.GetUnderlyingType(typeof(T)))) + { + case 1: + var b1 = ps.Span[offset]; + return *(T*)&b1; + case 2: + var b2 = ps.ReadU16Big(offset); + return *(T*)&b2; + case 4: + var b4 = ps.ReadU32Big(offset); + return *(T*)&b4; + case 8: + var b8 = ps.ReadU64Big(offset); + return *(T*)&b8; + default: + throw new ArgumentException("Enum is not of size 1, 2, 4, or 8.", nameof(T), null); + } + } + + private static void ReadBig(this PointerSpan ps, int offset, out T value) where T : unmanaged, Enum => + value = ps.ReadEnumBig(offset); + + private static void ReadBig(this PointerSpan ps, ref int offset, out T value) where T : unmanaged, Enum + { + value = ps.ReadEnumBig(offset); + offset += Unsafe.SizeOf(); + } + + private readonly unsafe struct PointerSpan : IList, IReadOnlyList, ICollection + where T : unmanaged + { + public readonly T* Pointer; + + public PointerSpan(T* pointer, int count) + { + this.Pointer = pointer; + this.Count = count; + } + + public PointerSpan(nint pointer, int count) + : this((T*)pointer, count) + { + } + + public Span Span => new(this.Pointer, this.Count); + + public bool IsEmpty => this.Count == 0; + + public int Count { get; } + + public int Length => this.Count; + + public int ByteCount => sizeof(T) * this.Count; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => this; + + bool ICollection.IsReadOnly => false; + + public ref T this[int index] => ref this.Pointer[this.EnsureIndex(index)]; + + public PointerSpan this[Range range] => this.Slice(range.GetOffsetAndLength(this.Count)); + + T IList.this[int index] + { + get => this.Pointer[this.EnsureIndex(index)]; + set => this.Pointer[this.EnsureIndex(index)] = value; + } + + T IReadOnlyList.this[int index] => this.Pointer[this.EnsureIndex(index)]; + + public bool ContainsPointer(T2* obj) where T2 : unmanaged => + (T*)obj >= this.Pointer && (T*)(obj + 1) <= this.Pointer + this.Count; + + public PointerSpan Slice(int offset, int count) => new(this.Pointer + offset, count); + + public PointerSpan Slice((int Offset, int Count) offsetAndCount) + => this.Slice(offsetAndCount.Offset, offsetAndCount.Count); + + public PointerSpan As(int count) + where T2 : unmanaged => + count > this.Count / sizeof(T2) + ? throw new ArgumentOutOfRangeException( + nameof(count), + count, + $"Wanted {count} items; had {this.Count / sizeof(T2)} items") + : new((T2*)this.Pointer, count); + + public PointerSpan As() + where T2 : unmanaged => + new((T2*)this.Pointer, this.Count / sizeof(T2)); + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + yield return this[i]; + } + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Contains(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this.Pointer[i], item)) + return true; + } + + return false; + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array[arrayIndex + i] = this.Pointer[i]; + } + + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + int IList.IndexOf(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this.Pointer[i], item)) + return i; + } + + return -1; + } + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array.SetValue(this.Pointer[i], arrayIndex + i); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => + index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); + } + + private readonly unsafe struct BigEndianPointerSpan + : IList, IReadOnlyList, ICollection + where T : unmanaged + { + public readonly T* Pointer; + + private readonly Func reverseEndianness; + + public BigEndianPointerSpan(PointerSpan pointerSpan, Func reverseEndianness) + { + this.reverseEndianness = reverseEndianness; + this.Pointer = pointerSpan.Pointer; + this.Count = pointerSpan.Count; + } + + public int Count { get; } + + public int Length => this.Count; + + public int ByteCount => sizeof(T) * this.Count; + + public bool IsSynchronized => true; + + public object SyncRoot => this; + + public bool IsReadOnly => true; + + public T this[int index] + { + get => + BitConverter.IsLittleEndian + ? this.reverseEndianness(this.Pointer[this.EnsureIndex(index)]) + : this.Pointer[this.EnsureIndex(index)]; + set => this.Pointer[this.EnsureIndex(index)] = + BitConverter.IsLittleEndian + ? this.reverseEndianness(value) + : value; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + yield return this[i]; + } + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Contains(T item) => throw new NotSupportedException(); + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array[arrayIndex + i] = this[i]; + } + + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + int IList.IndexOf(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this[i], item)) + return i; + } + + return -1; + } + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array.SetValue(this[i], arrayIndex + i); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => + index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs new file mode 100644 index 000000000..80cf4b7da --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs @@ -0,0 +1,1391 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] +[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] +internal static partial class TrueTypeUtils +{ + [Flags] + private enum ValueFormat : ushort + { + PlacementX = 1 << 0, + PlacementY = 1 << 1, + AdvanceX = 1 << 2, + AdvanceY = 1 << 3, + PlacementDeviceOffsetX = 1 << 4, + PlacementDeviceOffsetY = 1 << 5, + AdvanceDeviceOffsetX = 1 << 6, + AdvanceDeviceOffsetY = 1 << 7, + + ValidBits = 0 + | PlacementX | PlacementY + | AdvanceX | AdvanceY + | PlacementDeviceOffsetX | PlacementDeviceOffsetY + | AdvanceDeviceOffsetX | AdvanceDeviceOffsetY, + } + + private static int NumBytes(this ValueFormat value) => + ushort.PopCount((ushort)(value & ValueFormat.ValidBits)) * 2; + + private readonly struct Cmap + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/cmap + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html + + public static readonly TagStruct DirectoryTableTag = new('c', 'm', 'a', 'p'); + + public readonly PointerSpan Memory; + + public Cmap(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Cmap(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort RecordCount => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan Records => new( + this.Memory[4..].As(this.RecordCount), + EncodingRecord.ReverseEndianness); + + public EncodingRecord? UnicodeEncodingRecord => + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Bmp }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Full }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.UnicodeFullRepertoire }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeBmp }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeFullRepertoire }); + + public CmapFormat? UnicodeTable => this.GetTable(this.UnicodeEncodingRecord); + + public CmapFormat? GetTable(EncodingRecord? encodingRecord) => + encodingRecord is { } record + ? this.Memory.ReadU16Big(record.SubtableOffset) switch + { + 0 => new CmapFormat0(this.Memory[record.SubtableOffset..]), + 2 => new CmapFormat2(this.Memory[record.SubtableOffset..]), + 4 => new CmapFormat4(this.Memory[record.SubtableOffset..]), + 6 => new CmapFormat6(this.Memory[record.SubtableOffset..]), + 8 => new CmapFormat8(this.Memory[record.SubtableOffset..]), + 10 => new CmapFormat10(this.Memory[record.SubtableOffset..]), + 12 or 13 => new CmapFormat12And13(this.Memory[record.SubtableOffset..]), + _ => null, + } + : null; + + public struct EncodingRecord + { + public PlatformAndEncoding PlatformAndEncoding; + public int SubtableOffset; + + public EncodingRecord(PointerSpan span) + { + this.PlatformAndEncoding = new(span); + var offset = Unsafe.SizeOf(); + span.ReadBig(ref offset, out this.SubtableOffset); + } + + public static EncodingRecord ReverseEndianness(EncodingRecord value) => new() + { + PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), + SubtableOffset = BinaryPrimitives.ReverseEndianness(value.SubtableOffset), + }; + } + + public struct MapGroup : IComparable + { + public int StartCharCode; + public int EndCharCode; + public int GlyphId; + + public MapGroup(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.StartCharCode); + span.ReadBig(ref offset, out this.EndCharCode); + span.ReadBig(ref offset, out this.GlyphId); + } + + public static MapGroup ReverseEndianness(MapGroup obj) => new() + { + StartCharCode = BinaryPrimitives.ReverseEndianness(obj.StartCharCode), + EndCharCode = BinaryPrimitives.ReverseEndianness(obj.EndCharCode), + GlyphId = BinaryPrimitives.ReverseEndianness(obj.GlyphId), + }; + + public int CompareTo(MapGroup other) + { + var endCharCodeComparison = this.EndCharCode.CompareTo(other.EndCharCode); + if (endCharCodeComparison != 0) return endCharCodeComparison; + + var startCharCodeComparison = this.StartCharCode.CompareTo(other.StartCharCode); + if (startCharCodeComparison != 0) return startCharCodeComparison; + + return this.GlyphId.CompareTo(other.GlyphId); + } + } + + public abstract class CmapFormat : IReadOnlyDictionary + { + public int Count => this.Count(x => x.Value != 0); + + public IEnumerable Keys => this.Select(x => x.Key); + + public IEnumerable Values => this.Select(x => x.Value); + + public ushort this[int key] => throw new NotImplementedException(); + + public abstract ushort CharToGlyph(int c); + + public abstract IEnumerator> GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public bool ContainsKey(int key) => this.CharToGlyph(key) != 0; + + public bool TryGetValue(int key, out ushort value) + { + value = this.CharToGlyph(key); + return value != 0; + } + } + + public class CmapFormat0 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat0(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public PointerSpan GlyphIdArray => this.Memory.Slice(6, 256); + + public override ushort CharToGlyph(int c) => c is >= 0 and < 256 ? this.GlyphIdArray[c] : (byte)0; + + public override IEnumerator> GetEnumerator() + { + for (var codepoint = 0; codepoint < 256; codepoint++) + { + if (this.GlyphIdArray[codepoint] is var glyphId and not 0) + yield return new(codepoint, glyphId); + } + } + } + + public class CmapFormat2 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat2(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan SubHeaderKeys => new( + this.Memory[6..].As(256), + BinaryPrimitives.ReverseEndianness); + + public PointerSpan Data => this.Memory[518..]; + + public bool TryGetSubHeader( + int keyIndex, out SubHeader subheader, out BigEndianPointerSpan glyphSpan) + { + if (keyIndex < 0 || keyIndex >= this.SubHeaderKeys.Count) + { + subheader = default; + glyphSpan = default; + return false; + } + + var offset = this.SubHeaderKeys[keyIndex]; + if (offset + Unsafe.SizeOf() > this.Data.Length) + { + subheader = default; + glyphSpan = default; + return false; + } + + subheader = new(this.Data[offset..]); + glyphSpan = new( + this.Data[(offset + Unsafe.SizeOf() + subheader.IdRangeOffset)..] + .As(subheader.EntryCount), + BinaryPrimitives.ReverseEndianness); + + return true; + } + + public override ushort CharToGlyph(int c) + { + if (!this.TryGetSubHeader(c >> 8, out var sh, out var glyphSpan)) + return 0; + + c = (c & 0xFF) - sh.FirstCode; + if (c > 0 || c >= glyphSpan.Count) + return 0; + + var res = glyphSpan[c]; + return res == 0 ? (ushort)0 : unchecked((ushort)(res + sh.IdDelta)); + } + + public override IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.SubHeaderKeys.Count; i++) + { + if (!this.TryGetSubHeader(i, out var sh, out var glyphSpan)) + continue; + + for (var j = 0; j < glyphSpan.Count; j++) + { + var res = glyphSpan[j]; + if (res == 0) + continue; + + var glyphId = unchecked((ushort)(res + sh.IdDelta)); + if (glyphId == 0) + continue; + + var codepoint = (i << 8) | (sh.FirstCode + j); + yield return new(codepoint, glyphId); + } + } + } + + public struct SubHeader + { + public ushort FirstCode; + public ushort EntryCount; + public ushort IdDelta; + public ushort IdRangeOffset; + + public SubHeader(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.FirstCode); + span.ReadBig(ref offset, out this.EntryCount); + span.ReadBig(ref offset, out this.IdDelta); + span.ReadBig(ref offset, out this.IdRangeOffset); + } + } + } + + public class CmapFormat4 : CmapFormat + { + public const int EndCodesOffset = 14; + + public readonly PointerSpan Memory; + + public CmapFormat4(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public ushort SegCountX2 => this.Memory.ReadU16Big(6); + + public ushort SearchRange => this.Memory.ReadU16Big(8); + + public ushort EntrySelector => this.Memory.ReadU16Big(10); + + public ushort RangeShift => this.Memory.ReadU16Big(12); + + public BigEndianPointerSpan EndCodes => new( + this.Memory.Slice(EndCodesOffset, this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan StartCodes => new( + this.Memory.Slice(EndCodesOffset + 2 + (1 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan IdDeltas => new( + this.Memory.Slice(EndCodesOffset + 2 + (2 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan IdRangeOffsets => new( + this.Memory.Slice(EndCodesOffset + 2 + (3 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan GlyphIds => new( + this.Memory.Slice(EndCodesOffset + 2 + (4 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + if (c is < 0 or >= 0x10000) + return 0; + + var i = this.EndCodes.BinarySearch((ushort)c); + if (i < 0) + return 0; + + var startCode = this.StartCodes[i]; + var endCode = this.EndCodes[i]; + if (c < startCode || c > endCode) + return 0; + + var idRangeOffset = this.IdRangeOffsets[i]; + var idDelta = this.IdDeltas[i]; + if (idRangeOffset == 0) + return unchecked((ushort)(c + idDelta)); + + var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; + if (ptr > this.Memory.Length) + return 0; + + var glyphs = new BigEndianPointerSpan( + this.Memory[ptr..].As(endCode - startCode + 1), + BinaryPrimitives.ReverseEndianness); + + var glyph = glyphs[c - startCode]; + return unchecked(glyph == 0 ? (ushort)0 : (ushort)(idDelta + glyph)); + } + + public override IEnumerator> GetEnumerator() + { + var startCodes = this.StartCodes; + var endCodes = this.EndCodes; + var idDeltas = this.IdDeltas; + var idRangeOffsets = this.IdRangeOffsets; + + for (var i = 0; i < this.SegCountX2 / 2; i++) + { + var startCode = startCodes[i]; + var endCode = endCodes[i]; + var idRangeOffset = idRangeOffsets[i]; + var idDelta = idDeltas[i]; + + if (idRangeOffset == 0) + { + for (var c = (int)startCode; c <= endCode; c++) + yield return new(c, (ushort)(c + idDelta)); + } + else + { + var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; + if (ptr >= this.Memory.Length) + continue; + + var glyphs = new BigEndianPointerSpan( + this.Memory[ptr..].As(endCode - startCode + 1), + BinaryPrimitives.ReverseEndianness); + + for (var j = 0; j < glyphs.Count; j++) + { + var glyphId = glyphs[j]; + if (glyphId == 0) + continue; + + glyphId += idDelta; + if (glyphId == 0) + continue; + + yield return new(startCode + j, glyphId); + } + } + } + } + } + + public class CmapFormat6 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat6(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public ushort FirstCode => this.Memory.ReadU16Big(6); + + public ushort EntryCount => this.Memory.ReadU16Big(8); + + public BigEndianPointerSpan GlyphIds => new( + this.Memory[10..].As(this.EntryCount), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var glyphIds = this.GlyphIds; + if (c < this.FirstCode || c >= this.FirstCode + this.GlyphIds.Count) + return 0; + + return glyphIds[c - this.FirstCode]; + } + + public override IEnumerator> GetEnumerator() + { + var glyphIds = this.GlyphIds; + for (var i = 0; i < this.GlyphIds.Length; i++) + { + var g = glyphIds[i]; + if (g != 0) + yield return new(this.FirstCode + i, g); + } + } + } + + public class CmapFormat8 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat8(PointerSpan memory) => this.Memory = memory; + + public int Format => this.Memory.ReadI32Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public PointerSpan Is32 => this.Memory.Slice(12, 8192); + + public int NumGroups => this.Memory.ReadI32Big(8204); + + public BigEndianPointerSpan Groups => + new(this.Memory[8208..].As(), MapGroup.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var groups = this.Groups; + + var i = groups.BinarySearch((in MapGroup value) => c.CompareTo(value.EndCharCode)); + if (i < 0) + return 0; + + var group = groups[i]; + if (c < group.StartCharCode || c > group.EndCharCode) + return 0; + + return unchecked((ushort)(group.GlyphId + c - group.StartCharCode)); + } + + public override IEnumerator> GetEnumerator() + { + foreach (var group in this.Groups) + { + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + { + var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); + if (glyphId == 0) + continue; + + yield return new(j, glyphId); + } + } + } + } + + public class CmapFormat10 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat10(PointerSpan memory) => this.Memory = memory; + + public int Format => this.Memory.ReadI32Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public int StartCharCode => this.Memory.ReadI32Big(12); + + public int NumChars => this.Memory.ReadI32Big(16); + + public BigEndianPointerSpan GlyphIdArray => new( + this.Memory.Slice(20, this.NumChars * 2).As(), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + if (c < this.StartCharCode || c >= this.StartCharCode + this.GlyphIdArray.Count) + return 0; + + return this.GlyphIdArray[c]; + } + + public override IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.GlyphIdArray.Count; i++) + { + var glyph = this.GlyphIdArray[i]; + if (glyph != 0) + yield return new(this.StartCharCode + i, glyph); + } + } + } + + public class CmapFormat12And13 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat12And13(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public int NumGroups => this.Memory.ReadI32Big(12); + + public BigEndianPointerSpan Groups => new( + this.Memory[16..].As(this.NumGroups), + MapGroup.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var groups = this.Groups; + + var i = groups.BinarySearch(new MapGroup() { EndCharCode = c }); + if (i < 0) + return 0; + + var group = groups[i]; + if (c < group.StartCharCode || c > group.EndCharCode) + return 0; + + if (this.Format == 12) + return (ushort)(group.GlyphId + c - group.StartCharCode); + else + return (ushort)group.GlyphId; + } + + public override IEnumerator> GetEnumerator() + { + var groups = this.Groups; + if (this.Format == 12) + { + foreach (var group in groups) + { + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + { + var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); + if (glyphId == 0) + continue; + + yield return new(j, glyphId); + } + } + } + else + { + foreach (var group in groups) + { + if (group.GlyphId == 0) + continue; + + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + yield return new(j, (ushort)group.GlyphId); + } + } + } + } + } + + private readonly struct Gpos + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos + + public static readonly TagStruct DirectoryTableTag = new('G', 'P', 'O', 'S'); + + public readonly PointerSpan Memory; + + public Gpos(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Gpos(PointerSpan memory) => this.Memory = memory; + + public Fixed Version => new(this.Memory); + + public ushort ScriptListOffset => this.Memory.ReadU16Big(4); + + public ushort FeatureListOffset => this.Memory.ReadU16Big(6); + + public ushort LookupListOffset => this.Memory.ReadU16Big(8); + + public uint FeatureVariationsOffset => this.Version.CompareTo(new(1, 1)) >= 0 + ? this.Memory.ReadU32Big(10) + : 0; + + public BigEndianPointerSpan LookupOffsetList => new( + this.Memory[(this.LookupListOffset + 2)..].As( + this.Memory.ReadU16Big(this.LookupListOffset)), + BinaryPrimitives.ReverseEndianness); + + public IEnumerable EnumerateLookupTables() + { + foreach (var offset in this.LookupOffsetList) + yield return new(this.Memory[(this.LookupListOffset + offset)..]); + } + + public IEnumerable ExtractAdvanceX() => + this.EnumerateLookupTables() + .SelectMany( + lookupTable => lookupTable.Type switch + { + LookupType.PairAdjustment => + lookupTable.SelectMany(y => new PairAdjustmentPositioning(y).ExtractAdvanceX()), + LookupType.ExtensionPositioning => + lookupTable + .Where(y => y.ReadU16Big(0) == 1) + .Select(y => new ExtensionPositioningSubtableFormat1(y)) + .Where(y => y.ExtensionLookupType == LookupType.PairAdjustment) + .SelectMany(y => new PairAdjustmentPositioning(y.ExtensionData).ExtractAdvanceX()), + _ => Array.Empty(), + }); + + public struct ValueRecord + { + public short PlacementX; + public short PlacementY; + public short AdvanceX; + public short AdvanceY; + public short PlacementDeviceOffsetX; + public short PlacementDeviceOffsetY; + public short AdvanceDeviceOffsetX; + public short AdvanceDeviceOffsetY; + + public ValueRecord(PointerSpan pointerSpan, ValueFormat valueFormat) + { + var offset = 0; + if ((valueFormat & ValueFormat.PlacementX) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementX); + + if ((valueFormat & ValueFormat.PlacementY) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementY); + + if ((valueFormat & ValueFormat.AdvanceX) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceX); + if ((valueFormat & ValueFormat.AdvanceY) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceY); + if ((valueFormat & ValueFormat.PlacementDeviceOffsetX) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetX); + + if ((valueFormat & ValueFormat.PlacementDeviceOffsetY) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetY); + + if ((valueFormat & ValueFormat.AdvanceDeviceOffsetX) != 0) + pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetX); + + if ((valueFormat & ValueFormat.AdvanceDeviceOffsetY) != 0) + pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetY); + } + } + + public readonly struct PairAdjustmentPositioning + { + public readonly PointerSpan Memory; + + public PairAdjustmentPositioning(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public IEnumerable ExtractAdvanceX() => this.Format switch + { + 1 => new Format1(this.Memory).ExtractAdvanceX(), + 2 => new Format2(this.Memory).ExtractAdvanceX(), + _ => Array.Empty(), + }; + + public readonly struct Format1 + { + public readonly PointerSpan Memory; + + public Format1(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort CoverageOffset => this.Memory.ReadU16Big(2); + + public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); + + public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); + + public ushort PairSetCount => this.Memory.ReadU16Big(8); + + public BigEndianPointerSpan PairSetOffsets => new( + this.Memory[10..].As(this.PairSetCount), + BinaryPrimitives.ReverseEndianness); + + public CoverageTable CoverageTable => new(this.Memory[this.CoverageOffset..]); + + public PairSet this[int index] => new( + this.Memory[this.PairSetOffsets[index] ..], + this.ValueFormat1, + this.ValueFormat2); + + public IEnumerable ExtractAdvanceX() + { + if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && + (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) + { + yield break; + } + + var coverageTable = this.CoverageTable; + switch (coverageTable.Format) + { + case CoverageTable.CoverageFormat.Glyphs: + { + var glyphSpan = coverageTable.Glyphs; + foreach (var coverageIndex in Enumerable.Range(0, glyphSpan.Count)) + { + var glyph1Id = glyphSpan[coverageIndex]; + PairSet pairSetView; + try + { + pairSetView = this[coverageIndex]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) + { + var pair = pairSetView[pairIndex]; + var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); + if (adj >= 10000) + System.Diagnostics.Debugger.Break(); + + if (adj != 0) + yield return new(glyph1Id, pair.SecondGlyph, adj); + } + } + + break; + } + + case CoverageTable.CoverageFormat.RangeRecords: + { + foreach (var rangeRecord in coverageTable.RangeRecords) + { + var startGlyphId = rangeRecord.StartGlyphId; + var endGlyphId = rangeRecord.EndGlyphId; + var startCoverageIndex = rangeRecord.StartCoverageIndex; + var glyphCount = endGlyphId - startGlyphId + 1; + foreach (var glyph1Id in Enumerable.Range(startGlyphId, glyphCount)) + { + PairSet pairSetView; + try + { + pairSetView = this[startCoverageIndex + glyph1Id - startGlyphId]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) + { + var pair = pairSetView[pairIndex]; + var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); + if (adj != 0) + yield return new((ushort)glyph1Id, pair.SecondGlyph, adj); + } + } + } + + break; + } + } + } + + public readonly struct PairSet + { + public readonly PointerSpan Memory; + public readonly ValueFormat ValueFormat1; + public readonly ValueFormat ValueFormat2; + public readonly int PairValue1Size; + public readonly int PairValue2Size; + public readonly int PairSize; + + public PairSet( + PointerSpan memory, + ValueFormat valueFormat1, + ValueFormat valueFormat2) + { + this.Memory = memory; + this.ValueFormat1 = valueFormat1; + this.ValueFormat2 = valueFormat2; + this.PairValue1Size = this.ValueFormat1.NumBytes(); + this.PairValue2Size = this.ValueFormat2.NumBytes(); + this.PairSize = 2 + this.PairValue1Size + this.PairValue2Size; + } + + public ushort Count => this.Memory.ReadU16Big(0); + + public PairValueRecord this[int index] + { + get + { + var pvr = this.Memory.Slice(2 + (this.PairSize * index), this.PairSize); + return new() + { + SecondGlyph = pvr.ReadU16Big(0), + Record1 = new(pvr.Slice(2, this.PairValue1Size), this.ValueFormat1), + Record2 = new( + pvr.Slice(2 + this.PairValue1Size, this.PairValue2Size), + this.ValueFormat2), + }; + } + } + + public struct PairValueRecord + { + public ushort SecondGlyph; + public ValueRecord Record1; + public ValueRecord Record2; + } + } + } + + public readonly struct Format2 + { + public readonly PointerSpan Memory; + public readonly int PairValue1Size; + public readonly int PairValue2Size; + public readonly int PairSize; + + public Format2(PointerSpan memory) + { + this.Memory = memory; + this.PairValue1Size = this.ValueFormat1.NumBytes(); + this.PairValue2Size = this.ValueFormat2.NumBytes(); + this.PairSize = this.PairValue1Size + this.PairValue2Size; + } + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort CoverageOffset => this.Memory.ReadU16Big(2); + + public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); + + public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); + + public ushort ClassDef1Offset => this.Memory.ReadU16Big(8); + + public ushort ClassDef2Offset => this.Memory.ReadU16Big(10); + + public ushort Class1Count => this.Memory.ReadU16Big(12); + + public ushort Class2Count => this.Memory.ReadU16Big(14); + + public ClassDefTable ClassDefTable1 => new(this.Memory[this.ClassDef1Offset..]); + + public ClassDefTable ClassDefTable2 => new(this.Memory[this.ClassDef2Offset..]); + + public (ValueRecord Record1, ValueRecord Record2) this[(int Class1Index, int Class2Index) v] => + this[v.Class1Index, v.Class2Index]; + + public (ValueRecord Record1, ValueRecord Record2) this[int class1Index, int class2Index] + { + get + { + if (class1Index < 0 || class1Index >= this.Class1Count) + throw new IndexOutOfRangeException(); + + if (class2Index < 0 || class2Index >= this.Class2Count) + throw new IndexOutOfRangeException(); + + var offset = 16 + (this.PairSize * ((class1Index * this.Class2Count) + class2Index)); + return ( + new(this.Memory.Slice(offset, this.PairValue1Size), this.ValueFormat1), + new( + this.Memory.Slice(offset + this.PairValue1Size, this.PairValue2Size), + this.ValueFormat2)); + } + } + + public IEnumerable ExtractAdvanceX() + { + if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && + (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) + { + yield break; + } + + var classes1 = this.ClassDefTable1.Enumerate() + .GroupBy(x => x.Class, x => x.GlyphId) + .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); + + var classes2 = this.ClassDefTable2.Enumerate() + .GroupBy(x => x.Class, x => x.GlyphId) + .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); + + foreach (var class1 in Enumerable.Range(0, this.Class1Count)) + { + if (!classes1.TryGetValue((ushort)class1, out var glyphs1)) + continue; + + foreach (var class2 in Enumerable.Range(0, this.Class2Count)) + { + if (!classes2.TryGetValue((ushort)class2, out var glyphs2)) + continue; + + (ValueRecord, ValueRecord) record; + try + { + record = this[class1, class2]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + var val = record.Item1.AdvanceX + record.Item2.PlacementX; + if (val == 0) + continue; + + foreach (var glyph1 in glyphs1) + { + foreach (var glyph2 in glyphs2) + { + yield return new(glyph1, glyph2, (short)val); + } + } + } + } + } + } + } + + public readonly struct ExtensionPositioningSubtableFormat1 + { + public readonly PointerSpan Memory; + + public ExtensionPositioningSubtableFormat1(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public LookupType ExtensionLookupType => this.Memory.ReadEnumBig(2); + + public int ExtensionOffset => this.Memory.ReadI32Big(4); + + public PointerSpan ExtensionData => this.Memory[this.ExtensionOffset..]; + } + } + + private readonly struct Head + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/head + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html + + public const uint MagicNumberValue = 0x5F0F3CF5; + public static readonly TagStruct DirectoryTableTag = new('h', 'e', 'a', 'd'); + + public readonly PointerSpan Memory; + + public Head(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Head(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum HeadFlags : ushort + { + BaselineForFontAtZeroY = 1 << 0, + LeftSideBearingAtZeroX = 1 << 1, + InstructionsDependOnPointSize = 1 << 2, + ForcePpemsInteger = 1 << 3, + InstructionsAlterAdvanceWidth = 1 << 4, + VerticalLayout = 1 << 5, + Reserved6 = 1 << 6, + RequiresLayoutForCorrectLinguisticRendering = 1 << 7, + IsAatFont = 1 << 8, + ContainsRtlGlyph = 1 << 9, + ContainsIndicStyleRearrangementEffects = 1 << 10, + Lossless = 1 << 11, + ProduceCompatibleMetrics = 1 << 12, + OptimizedForClearType = 1 << 13, + IsLastResortFont = 1 << 14, + Reserved15 = 1 << 15, + } + + [Flags] + public enum MacStyleFlags : ushort + { + Bold = 1 << 0, + Italic = 1 << 1, + Underline = 1 << 2, + Outline = 1 << 3, + Shadow = 1 << 4, + Condensed = 1 << 5, + Extended = 1 << 6, + } + + public Fixed Version => new(this.Memory); + + public Fixed FontRevision => new(this.Memory[4..]); + + public uint ChecksumAdjustment => this.Memory.ReadU32Big(8); + + public uint MagicNumber => this.Memory.ReadU32Big(12); + + public HeadFlags Flags => this.Memory.ReadEnumBig(16); + + public ushort UnitsPerEm => this.Memory.ReadU16Big(18); + + public ulong CreatedTimestamp => this.Memory.ReadU64Big(20); + + public ulong ModifiedTimestamp => this.Memory.ReadU64Big(28); + + public ushort MinX => this.Memory.ReadU16Big(36); + + public ushort MinY => this.Memory.ReadU16Big(38); + + public ushort MaxX => this.Memory.ReadU16Big(40); + + public ushort MaxY => this.Memory.ReadU16Big(42); + + public MacStyleFlags MacStyle => this.Memory.ReadEnumBig(44); + + public ushort LowestRecommendedPpem => this.Memory.ReadU16Big(46); + + public ushort FontDirectionHint => this.Memory.ReadU16Big(48); + + public ushort IndexToLocFormat => this.Memory.ReadU16Big(50); + + public ushort GlyphDataFormat => this.Memory.ReadU16Big(52); + } + + private readonly struct Kern + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/kern + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html + + public static readonly TagStruct DirectoryTableTag = new('k', 'e', 'r', 'n'); + + public readonly PointerSpan Memory; + + public Kern(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Kern(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public IEnumerable EnumerateHorizontalPairs() => this.Version switch + { + 0 => new Version0(this.Memory).EnumerateHorizontalPairs(), + 1 => new Version1(this.Memory).EnumerateHorizontalPairs(), + _ => Array.Empty(), + }; + + public readonly struct Format0 + { + public readonly PointerSpan Memory; + + public Format0(PointerSpan memory) => this.Memory = memory; + + public ushort PairCount => this.Memory.ReadU16Big(0); + + public ushort SearchRange => this.Memory.ReadU16Big(2); + + public ushort EntrySelector => this.Memory.ReadU16Big(4); + + public ushort RangeShift => this.Memory.ReadU16Big(6); + + public BigEndianPointerSpan Pairs => new( + this.Memory[8..].As(this.PairCount), + KerningPair.ReverseEndianness); + } + + public readonly struct Version0 + { + public readonly PointerSpan Memory; + + public Version0(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum CoverageFlags : byte + { + Horizontal = 1 << 0, + Minimum = 1 << 1, + CrossStream = 1 << 2, + Override = 1 << 3, + } + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort NumSubtables => this.Memory.ReadU16Big(2); + + public PointerSpan Data => this.Memory[4..]; + + public IEnumerable EnumerateSubtables() + { + var data = this.Data; + for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) + { + var st = new Subtable(data); + data = data[st.Length..]; + yield return st; + } + } + + public IEnumerable EnumerateHorizontalPairs() + { + var accumulator = new Dictionary<(ushort Left, ushort Right), short>(); + foreach (var subtable in this.EnumerateSubtables()) + { + var isOverride = (subtable.Flags & CoverageFlags.Override) != 0; + var isMinimum = (subtable.Flags & CoverageFlags.Minimum) != 0; + foreach (var t in subtable.EnumeratePairs()) + { + if (isOverride) + { + accumulator[(t.Left, t.Right)] = t.Value; + } + else if (isMinimum) + { + accumulator[(t.Left, t.Right)] = Math.Max( + accumulator.GetValueOrDefault((t.Left, t.Right), t.Value), + t.Value); + } + else + { + accumulator[(t.Left, t.Right)] = (short)( + accumulator.GetValueOrDefault( + (t.Left, t.Right)) + t.Value); + } + } + } + + return accumulator.Select( + x => new KerningPair { Left = x.Key.Left, Right = x.Key.Right, Value = x.Value }); + } + + public readonly struct Subtable + { + public readonly PointerSpan Memory; + + public Subtable(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public byte Format => this.Memory[4]; + + public CoverageFlags Flags => this.Memory.ReadEnumBig(5); + + public PointerSpan Data => this.Memory[6..]; + + public IEnumerable EnumeratePairs() => this.Format switch + { + 0 => new Format0(this.Data).Pairs, + _ => Array.Empty(), + }; + } + } + + public readonly struct Version1 + { + public readonly PointerSpan Memory; + + public Version1(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum CoverageFlags : byte + { + Vertical = 1 << 0, + CrossStream = 1 << 1, + Variation = 1 << 2, + } + + public Fixed Version => new(this.Memory); + + public int NumSubtables => this.Memory.ReadI16Big(4); + + public PointerSpan Data => this.Memory[8..]; + + public IEnumerable EnumerateSubtables() + { + var data = this.Data; + for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) + { + var st = new Subtable(data); + data = data[st.Length..]; + yield return st; + } + } + + public IEnumerable EnumerateHorizontalPairs() => this + .EnumerateSubtables() + .Where(x => x.Flags == 0) + .SelectMany(x => x.EnumeratePairs()); + + public readonly struct Subtable + { + public readonly PointerSpan Memory; + + public Subtable(PointerSpan memory) => this.Memory = memory; + + public int Length => this.Memory.ReadI32Big(0); + + public byte Format => this.Memory[4]; + + public CoverageFlags Flags => this.Memory.ReadEnumBig(5); + + public ushort TupleIndex => this.Memory.ReadU16Big(6); + + public PointerSpan Data => this.Memory[8..]; + + public IEnumerable EnumeratePairs() => this.Format switch + { + 0 => new Format0(this.Data).Pairs, + _ => Array.Empty(), + }; + } + } + } + + private readonly struct Name + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/name + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html + + public static readonly TagStruct DirectoryTableTag = new('n', 'a', 'm', 'e'); + + public readonly PointerSpan Memory; + + public Name(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Name(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort Count => this.Memory.ReadU16Big(2); + + public ushort StorageOffset => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan NameRecords => new( + this.Memory[6..].As(this.Count), + NameRecord.ReverseEndianness); + + public ushort LanguageCount => + this.Version == 0 ? (ushort)0 : this.Memory.ReadU16Big(6 + this.NameRecords.ByteCount); + + public BigEndianPointerSpan LanguageRecords => this.Version == 0 + ? default + : new( + this.Memory[ + (8 + this.NameRecords + .ByteCount)..] + .As( + this.LanguageCount), + LanguageRecord.ReverseEndianness); + + public PointerSpan Storage => this.Memory[this.StorageOffset..]; + + public string this[in NameRecord record] => + record.PlatformAndEncoding.Decode(this.Storage.Span.Slice(record.StringOffset, record.Length)); + + public string this[in LanguageRecord record] => + Encoding.ASCII.GetString(this.Storage.Span.Slice(record.LanguageTagOffset, record.Length)); + + public struct NameRecord + { + public PlatformAndEncoding PlatformAndEncoding; + public ushort LanguageId; + public NameId NameId; + public ushort Length; + public ushort StringOffset; + + public NameRecord(PointerSpan span) + { + this.PlatformAndEncoding = new(span); + var offset = Unsafe.SizeOf(); + span.ReadBig(ref offset, out this.LanguageId); + span.ReadBig(ref offset, out this.NameId); + span.ReadBig(ref offset, out this.Length); + span.ReadBig(ref offset, out this.StringOffset); + } + + public static NameRecord ReverseEndianness(NameRecord value) => new() + { + PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), + LanguageId = BinaryPrimitives.ReverseEndianness(value.LanguageId), + NameId = (NameId)BinaryPrimitives.ReverseEndianness((ushort)value.NameId), + Length = BinaryPrimitives.ReverseEndianness(value.Length), + StringOffset = BinaryPrimitives.ReverseEndianness(value.StringOffset), + }; + } + + public struct LanguageRecord + { + public ushort Length; + public ushort LanguageTagOffset; + + public LanguageRecord(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Length); + span.ReadBig(ref offset, out this.LanguageTagOffset); + } + + public static LanguageRecord ReverseEndianness(LanguageRecord value) => new() + { + Length = BinaryPrimitives.ReverseEndianness(value.Length), + LanguageTagOffset = BinaryPrimitives.ReverseEndianness(value.LanguageTagOffset), + }; + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs new file mode 100644 index 000000000..1d437d56d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs @@ -0,0 +1,135 @@ +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + /// + /// Checks whether the given will fail in , + /// and throws an appropriate exception if it is the case. + /// + /// The font config. + public static unsafe void CheckImGuiCompatibleOrThrow(in ImFontConfig fontConfig) + { + var ranges = fontConfig.GlyphRanges; + var sfnt = AsSfntFile(fontConfig); + var cmap = new Cmap(sfnt); + if (cmap.UnicodeTable is not { } unicodeTable) + throw new NotSupportedException("The font does not have a compatible Unicode character mapping table."); + if (unicodeTable.All(x => !ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(x.Key, ranges))) + throw new NotSupportedException("The font does not have any glyph that falls under the requested range."); + } + + /// + /// Enumerates through horizontal pair adjustments of a kern and gpos tables. + /// + /// The font config. + /// The enumerable of pair adjustments. Distance values need to be multiplied by font size in pixels. + public static IEnumerable<(char Left, char Right, float Distance)> ExtractHorizontalPairAdjustments( + ImFontConfig fontConfig) + { + float multiplier; + Dictionary glyphToCodepoints; + Gpos gpos = default; + Kern kern = default; + + try + { + var sfnt = AsSfntFile(fontConfig); + var head = new Head(sfnt); + multiplier = 3f / 4 / head.UnitsPerEm; + + if (new Cmap(sfnt).UnicodeTable is not { } table) + yield break; + + if (sfnt.ContainsKey(Kern.DirectoryTableTag)) + kern = new(sfnt); + else if (sfnt.ContainsKey(Gpos.DirectoryTableTag)) + gpos = new(sfnt); + else + yield break; + + glyphToCodepoints = table + .GroupBy(x => x.Value, x => x.Key) + .OrderBy(x => x.Key) + .ToDictionary( + x => x.Key, + x => x.Where(y => y <= ushort.MaxValue) + .Select(y => (char)y) + .ToArray()); + } + catch + { + // don't care; give up + yield break; + } + + if (kern.Memory.Count != 0) + { + foreach (var pair in kern.EnumerateHorizontalPairs()) + { + if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) + continue; + if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) + continue; + + foreach (var l in leftChars) + { + foreach (var r in rightChars) + yield return (l, r, pair.Value * multiplier); + } + } + } + else if (gpos.Memory.Count != 0) + { + foreach (var pair in gpos.ExtractAdvanceX()) + { + if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) + continue; + if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) + continue; + + foreach (var l in leftChars) + { + foreach (var r in rightChars) + yield return (l, r, pair.Value * multiplier); + } + } + } + } + + private static unsafe SfntFile AsSfntFile(in ImFontConfig fontConfig) + { + var memory = new PointerSpan((byte*)fontConfig.FontData, fontConfig.FontDataSize); + if (memory.Length < 4) + throw new NotSupportedException("File is too short to even have a magic."); + + var magic = memory.ReadU32Big(0); + if (BitConverter.IsLittleEndian) + magic = BinaryPrimitives.ReverseEndianness(magic); + + if (magic == SfntFile.FileTagTrueType1.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagType1.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagOpenTypeWithCff.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagOpenType1_0.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagTrueTypeApple.NativeValue) + return new(memory); + if (magic == TtcFile.FileTag.NativeValue) + return new TtcFile(memory)[fontConfig.FontNo]; + + throw new NotSupportedException($"The given file with the magic 0x{magic:X08} is not supported."); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs new file mode 100644 index 000000000..cb7f7c65a --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -0,0 +1,306 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Managed version of , to avoid unnecessary heap allocation and use of unsafe blocks. +/// +public struct SafeFontConfig +{ + /// + /// The raw config. + /// + public ImFontConfig Raw; + + /// + /// Initializes a new instance of the struct. + /// + public SafeFontConfig() + { + this.OversampleH = 1; + this.OversampleV = 1; + this.PixelSnapH = true; + this.GlyphMaxAdvanceX = float.MaxValue; + this.RasterizerMultiply = 1f; + this.RasterizerGamma = 1.4f; + this.EllipsisChar = unchecked((char)-1); + this.Raw.FontDataOwnedByAtlas = 1; + } + + /// + /// Initializes a new instance of the struct, + /// copying applicable values from an existing instance of . + /// + /// Config to copy from. + public unsafe SafeFontConfig(ImFontConfigPtr config) + : this() + { + if (config.NativePtr is not null) + { + this.Raw = *config.NativePtr; + this.Raw.GlyphRanges = null; + } + } + + /// + /// Gets or sets the index of font within a TTF/OTF file. + /// + public int FontNo + { + get => this.Raw.FontNo; + set => this.Raw.FontNo = EnsureRange(value, 0, int.MaxValue); + } + + /// + /// Gets or sets the desired size of the new font, in pixels.
+ /// Effectively, this is the line height.
+ /// Value is tied with . + ///
+ public float SizePx + { + get => this.Raw.SizePixels; + set => this.Raw.SizePixels = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the desired size of the new font, in points.
+ /// Effectively, this is the line height.
+ /// Value is tied with . + ///
+ public float SizePt + { + get => (this.Raw.SizePixels * 3) / 4; + set => this.Raw.SizePixels = EnsureRange((value * 4) / 3, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the horizontal oversampling pixel count.
+ /// Rasterize at higher quality for sub-pixel positioning.
+ /// Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory.
+ /// Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + ///
+ public int OversampleH + { + get => this.Raw.OversampleH; + set => this.Raw.OversampleH = EnsureRange(value, 1, int.MaxValue); + } + + /// + /// Gets or sets the vertical oversampling pixel count.
+ /// Rasterize at higher quality for sub-pixel positioning.
+ /// This is not really useful as we don't use sub-pixel positions on the Y axis. + ///
+ public int OversampleV + { + get => this.Raw.OversampleV; + set => this.Raw.OversampleV = EnsureRange(value, 1, int.MaxValue); + } + + /// + /// Gets or sets a value indicating whether to align every glyph to pixel boundary.
+ /// Useful e.g. if you are merging a non-pixel aligned font with the default font.
+ /// If enabled, you can set and to 1. + ///
+ public bool PixelSnapH + { + get => this.Raw.PixelSnapH != 0; + set => this.Raw.PixelSnapH = value ? (byte)1 : (byte)0; + } + + /// + /// Gets or sets the extra spacing (in pixels) between glyphs.
+ /// Only X axis is supported for now.
+ /// Effectively, it is the letter spacing. + ///
+ public Vector2 GlyphExtraSpacing + { + get => this.Raw.GlyphExtraSpacing; + set => this.Raw.GlyphExtraSpacing = new( + EnsureRange(value.X, float.MinValue, float.MaxValue), + EnsureRange(value.Y, float.MinValue, float.MaxValue)); + } + + /// + /// Gets or sets the offset all glyphs from this font input.
+ /// Use this to offset fonts vertically when merging multiple fonts. + ///
+ public Vector2 GlyphOffset + { + get => this.Raw.GlyphOffset; + set => this.Raw.GlyphOffset = new( + EnsureRange(value.X, float.MinValue, float.MaxValue), + EnsureRange(value.Y, float.MinValue, float.MaxValue)); + } + + /// + /// Gets or sets the glyph ranges, which is a user-provided list of Unicode range. + /// Each range has 2 values, and values are inclusive.
+ /// The list must be zero-terminated.
+ /// If empty or null, then all the glyphs from the font that is in the range of UCS-2 will be added. + ///
+ public ushort[]? GlyphRanges { get; set; } + + /// + /// Gets or sets the minimum AdvanceX for glyphs.
+ /// Set only to align font icons.
+ /// Set both / to enforce mono-space font. + ///
+ public float GlyphMinAdvanceX + { + get => this.Raw.GlyphMinAdvanceX; + set => this.Raw.GlyphMinAdvanceX = + float.IsFinite(value) + ? value + : throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); + } + + /// + /// Gets or sets the maximum AdvanceX for glyphs. + /// + public float GlyphMaxAdvanceX + { + get => this.Raw.GlyphMaxAdvanceX; + set => this.Raw.GlyphMaxAdvanceX = + float.IsFinite(value) + ? value + : throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); + } + + /// + /// Gets or sets a value that either brightens (>1.0f) or darkens (<1.0f) the font output.
+ /// Brightening small fonts may be a good workaround to make them more readable. + ///
+ public float RasterizerMultiply + { + get => this.Raw.RasterizerMultiply; + set => this.Raw.RasterizerMultiply = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the gamma value for fonts. + /// + public float RasterizerGamma + { + get => this.Raw.RasterizerGamma; + set => this.Raw.RasterizerGamma = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets a value explicitly specifying unicode codepoint of the ellipsis character.
+ /// When fonts are being merged first specified ellipsis will be used. + ///
+ public char EllipsisChar + { + get => (char)this.Raw.EllipsisChar; + set => this.Raw.EllipsisChar = value; + } + + /// + /// Gets or sets the desired name of the new font. Names longer than 40 bytes will be partially lost. + /// + public unsafe string Name + { + get + { + fixed (void* pName = this.Raw.Name) + { + var span = new ReadOnlySpan(pName, 40); + var firstNull = span.IndexOf((byte)0); + if (firstNull != -1) + span = span[..firstNull]; + return Encoding.UTF8.GetString(span); + } + } + + set + { + fixed (void* pName = this.Raw.Name) + { + var span = new Span(pName, 40); + Encoding.UTF8.GetBytes(value, span); + } + } + } + + /// + /// Gets or sets the desired font to merge with, if set. + /// + public unsafe ImFontPtr MergeFont + { + get => this.Raw.DstFont is not null ? this.Raw.DstFont : default; + set + { + this.Raw.MergeMode = value.NativePtr is null ? (byte)0 : (byte)1; + this.Raw.DstFont = value.NativePtr is null ? default : value.NativePtr; + } + } + + /// + /// Throws with appropriate messages, + /// if this has invalid values. + /// + public readonly void ThrowOnInvalidValues() + { + if (!(this.Raw.FontNo >= 0)) + throw new ArgumentException($"{nameof(this.FontNo)} must not be a negative number."); + + if (!(this.Raw.SizePixels > 0)) + throw new ArgumentException($"{nameof(this.SizePx)} must be a positive number."); + + if (!(this.Raw.OversampleH >= 1)) + throw new ArgumentException($"{nameof(this.OversampleH)} must be a negative number."); + + if (!(this.Raw.OversampleV >= 1)) + throw new ArgumentException($"{nameof(this.OversampleV)} must be a negative number."); + + if (!float.IsFinite(this.Raw.GlyphMinAdvanceX)) + throw new ArgumentException($"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); + + if (!float.IsFinite(this.Raw.GlyphMaxAdvanceX)) + throw new ArgumentException($"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); + + if (!(this.Raw.RasterizerMultiply > 0)) + throw new ArgumentException($"{nameof(this.RasterizerMultiply)} must be a positive number."); + + if (!(this.Raw.RasterizerGamma > 0)) + throw new ArgumentException($"{nameof(this.RasterizerGamma)} must be a positive number."); + + if (this.GlyphRanges is { Length: > 0 } ranges) + { + if (ranges[0] == 0) + { + throw new ArgumentException( + "Font ranges cannot start with 0.", + nameof(this.GlyphRanges)); + } + + if (ranges[(ranges.Length - 1) & ~1] != 0) + { + throw new ArgumentException( + "Font ranges must terminate with a zero at even indices.", + nameof(this.GlyphRanges)); + } + } + } + + private static T EnsureRange(T value, T min, T max, [CallerMemberName] string callerName = "") + where T : INumber + { + if (value < min) + throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be less than {min}."); + if (value > max) + throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be more than {max}."); + + return value; + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index dd2e5bad3..a477ec09e 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -12,6 +11,8 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -30,11 +31,13 @@ public sealed class UiBuilder : IDisposable private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); - private readonly GameFontManager gameFontManager = Service.Get(); + private readonly Framework framework = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -45,14 +48,32 @@ public sealed class UiBuilder : IDisposable /// The plugin namespace. internal UiBuilder(string namespaceName) { - this.stopwatch = new Stopwatch(); - this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); - this.namespaceName = namespaceName; + try + { + this.stopwatch = new Stopwatch(); + this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); + this.namespaceName = namespaceName; - this.interfaceManager.Draw += this.OnDraw; - this.interfaceManager.BuildFonts += this.OnBuildFonts; - this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; - this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.interfaceManager.Draw += this.OnDraw; + this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw); + + this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); + + this.FontAtlas = + this.scopedFinalizer + .Add( + Service + .Get() + .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); + this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; + this.FontAtlas.RebuildRecommend += this.RebuildFonts; + } + catch + { + this.scopedFinalizer.Dispose(); + throw; + } } /// @@ -80,19 +101,19 @@ public sealed class UiBuilder : IDisposable /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler.
- /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + /// pointers inside this handler. ///
- public event Action BuildFonts; + [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + public event Action? BuildFonts; /// /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler.
- /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + /// pointers inside this handler. ///
- public event Action AfterBuildFonts; + [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + public event Action? AfterBuildFonts; /// /// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown. @@ -107,18 +128,57 @@ public sealed class UiBuilder : IDisposable public event Action HideUi; /// - /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. + /// Gets the default Dalamud font size in points. /// + public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt; + + /// + /// Gets the default Dalamud font size in pixels. + /// + public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx; + + /// + /// Gets the default Dalamud font - supporting all game languages and icons.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// fontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); + /// + /// public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; /// - /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
+ /// Accessing this static property outside of is dangerous and not supported. ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// fontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// + /// public static ImFontPtr IconFont => InterfaceManager.IconFont; /// - /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
+ /// Accessing this static property outside of is dangerous and not supported. ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// fontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddDalamudAssetFont( + /// DalamudAsset.InconsolataRegular, + /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// + /// public static ImFontPtr MonoFont => InterfaceManager.MonoFont; /// @@ -190,6 +250,11 @@ public sealed class UiBuilder : IDisposable /// public bool UiPrepared => Service.GetNullable() != null; + /// + /// Gets the plugin-private font atlas. + /// + public IFontAtlas FontAtlas { get; } + /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// @@ -319,7 +384,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) .Unwrap(); } else @@ -341,7 +406,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) .Unwrap(); } else @@ -357,19 +422,49 @@ public sealed class UiBuilder : IDisposable ///
/// Font to get. /// Handle to the game font which may or may not be available for use yet. - public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style); + [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] + public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( + (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), + Service.Get()); /// /// Call this to queue a rebuild of the font atlas.
- /// This will invoke any handlers and ensure that any loaded fonts are - /// ready to be used on the next UI frame. + /// This will invoke any and handlers and ensure that any + /// loaded fonts are ready to be used on the next UI frame. ///
public void RebuildFonts() { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); - this.interfaceManager.RebuildFonts(); + if (this.AfterBuildFonts is null && this.BuildFonts is null) + this.FontAtlas.BuildFontsAsync(); + else + this.FontAtlas.BuildFontsOnNextFrame(); } + /// + /// Creates an isolated . + /// + /// Specify when and how to rebuild this atlas. + /// Whether the fonts in the atlas is global scaled. + /// Name for debugging purposes. + /// A new instance of . + /// + /// Use this to create extra font atlases, if you want to create and dispose fonts without having to rebuild all + /// other fonts together.
+ /// If is not , + /// the font rebuilding functions must be called manually. + ///
+ public IFontAtlas CreateFontAtlas( + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled = true, + string? debugName = null) => + this.scopedFinalizer.Add(Service + .Get() + .CreateFontAtlas( + this.namespaceName + ":" + (debugName ?? "custom"), + autoRebuildMode, + isGlobalScaled)); + /// /// Add a notification to the notification queue. /// @@ -392,12 +487,7 @@ public sealed class UiBuilder : IDisposable /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() - { - this.interfaceManager.Draw -= this.OnDraw; - this.interfaceManager.BuildFonts -= this.OnBuildFonts; - this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; - } + void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); /// /// Open the registered configuration UI, if it exists. @@ -463,8 +553,12 @@ public sealed class UiBuilder : IDisposable this.ShowUi?.InvokeSafely(); } - if (!this.interfaceManager.FontsReady) + // just in case, if something goes wrong, prevent drawing; otherwise it probably will crash. + if (!this.FontAtlas.BuildTask.IsCompletedSuccessfully + && (this.BuildFonts is not null || this.AfterBuildFonts is not null)) + { return; + } ImGui.PushID(this.namespaceName); if (DoStats) @@ -526,14 +620,28 @@ public sealed class UiBuilder : IDisposable this.hitchDetector.Stop(); } - private void OnBuildFonts() + private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e) { - this.BuildFonts?.InvokeSafely(); - } + if (e.IsAsyncBuildOperation) + return; - private void OnAfterBuildFonts() - { - this.AfterBuildFonts?.InvokeSafely(); + e.OnPreBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + this.BuildFonts?.InvokeSafely(); + ImGui.GetIO().NativePtr->Fonts = prev; + }); + + e.OnPostBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + this.AfterBuildFonts?.InvokeSafely(); + ImGui.GetIO().NativePtr->Fonts = prev; + }); } private void OnResizeBuffers() diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index ad151ec4e..444463d41 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,10 +1,15 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Numerics; +using System.Reactive.Disposables; using System.Runtime.InteropServices; +using System.Text.Unicode; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using ImGuiScene; @@ -31,8 +36,7 @@ public static class ImGuiHelpers /// This does not necessarily mean you can call drawing functions. /// public static unsafe bool IsImGuiInitialized => - ImGui.GetCurrentContext() is not (nint)0 // KW: IDEs get mad without the cast, despite being unnecessary - && ImGui.GetIO().NativePtr is not null; + ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null; ///
/// Gets the global Dalamud scale; even available before drawing is ready.
@@ -198,7 +202,7 @@ public static class ImGuiHelpers /// If a positive number is given, numbers will be rounded to this. public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f) { - Func rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x; + Func rounder = round > 0 ? x => MathF.Round(x / round) * round : x => x; var font = fontPtr.NativePtr; font->FontSize = rounder(font->FontSize * scale); @@ -310,6 +314,7 @@ public static class ImGuiHelpers glyph->U1, glyph->V1, glyph->AdvanceX * scale); + target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint); changed = true; } else if (!missingOnly) @@ -343,25 +348,18 @@ public static class ImGuiHelpers } if (changed && rebuildLookupTable) - target.BuildLookupTableNonstandard(); - } + { + // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. + // FallbackGlyph is resolved after resolving ' '. + // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, + // making FindGlyph return nullptr. + // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, + // making ImGui attempt to treat whatever was there as a ' '. + // This may cause random glyphs to be sized randomly, if not an access violation exception. + target.NativePtr->FallbackGlyph = null; - /// - /// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions. - /// - /// The font. - public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font) - { - // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. - // FallbackGlyph is resolved after resolving ' '. - // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, - // making FindGlyph return nullptr. - // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, - // making ImGui attempt to treat whatever was there as a ' '. - // This may cause random glyphs to be sized randomly, if not an access violation exception. - font.NativePtr->FallbackGlyph = null; - - font.BuildLookupTable(); + target.BuildLookupTable(); + } } /// @@ -407,6 +405,103 @@ public static class ImGuiHelpers public static void CenterCursorFor(float itemWidth) => ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); + /// + /// Allocates memory on the heap using
+ /// Memory must be freed using . + ///
+ /// Note that null is a valid return value when is 0. + ///
+ /// The length of allocated memory. + /// The allocated memory. + /// If returns null. + public static unsafe void* AllocateMemory(int length) + { + // TODO: igMemAlloc takes size_t, which is nint; ImGui.NET apparently interpreted that as uint. + // fix that in ImGui.NET. + switch (length) + { + case 0: + return null; + case < 0: + throw new ArgumentOutOfRangeException( + nameof(length), + length, + $"{nameof(length)} cannot be a negative number."); + default: + var memory = ImGuiNative.igMemAlloc((uint)length); + if (memory is null) + { + throw new OutOfMemoryException( + $"Failed to allocate {length} bytes using {nameof(ImGuiNative.igMemAlloc)}"); + } + + return memory; + } + } + + /// + /// Creates a new instance of with a natively backed memory. + /// + /// The created instance. + /// Disposable you can call. + public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder) + { + builder = new(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder()); + var ptr = builder.NativePtr; + return Disposable.Create(() => + { + if (ptr != null) + ImGuiNative.ImFontGlyphRangesBuilder_destroy(ptr); + ptr = null; + }); + } + + /// + /// Builds ImGui Glyph Ranges for use with . + /// + /// The builder. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// When disposed, the resource allocated for the range will be freed. + public static unsafe ushort[] BuildRangesToArray( + this ImFontGlyphRangesBuilderPtr builder, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + if (addFallbackCodepoints) + builder.AddText(FontAtlasFactory.FallbackCodepoints); + if (addEllipsisCodepoints) + { + builder.AddText(FontAtlasFactory.EllipsisCodepoints); + builder.AddChar('.'); + } + + builder.BuildRanges(out var vec); + return new ReadOnlySpan((void*)vec.Data, vec.Size).ToArray(); + } + + /// + public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges) + => CreateImGuiRangesFrom((IEnumerable)ranges); + + /// + /// Creates glyph ranges from .
+ /// Use values from . + ///
+ /// The unicode ranges. + /// The range array that can be used for . + public static ushort[] CreateImGuiRangesFrom(IEnumerable ranges) => + ranges + .Where(x => x.FirstCodePoint <= ushort.MaxValue) + .SelectMany( + x => new[] + { + (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), + (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), + }) + .Append((ushort)0) + .ToArray(); + /// /// Determines whether is empty. /// @@ -415,7 +510,7 @@ public static class ImGuiHelpers public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; /// - /// Determines whether is not null and loaded. + /// Determines whether is empty. /// /// The pointer. /// Whether it is empty. @@ -427,6 +522,27 @@ public static class ImGuiHelpers /// The pointer. /// Whether it is empty. public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; + + /// + /// If is default, then returns . + /// + /// The self. + /// The other. + /// if it is not default; otherwise, . + public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => + self.NativePtr is null ? other : self; + + /// + /// Mark 4K page as used, after adding a codepoint to a font. + /// + /// The font. + /// The codepoint. + internal static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) + { + // Mark 4K page as used + var pageIndex = unchecked((ushort)(codepoint / 4096)); + font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); + } /// /// Finds the corresponding ImGui viewport ID for the given window handle. @@ -448,6 +564,89 @@ public static class ImGuiHelpers return -1; } + /// + /// Attempts to validate that is valid. + /// + /// The font pointer. + /// The exception, if any occurred during validation. + internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr) + { + try + { + var font = fontPtr.NativePtr; + if (font is null) + throw new NullReferenceException("The font is null."); + + _ = Marshal.ReadIntPtr((nint)font); + if (font->IndexedHotData.Data != 0) + _ = Marshal.ReadIntPtr(font->IndexedHotData.Data); + if (font->FrequentKerningPairs.Data != 0) + _ = Marshal.ReadIntPtr(font->FrequentKerningPairs.Data); + if (font->IndexLookup.Data != 0) + _ = Marshal.ReadIntPtr(font->IndexLookup.Data); + if (font->Glyphs.Data != 0) + _ = Marshal.ReadIntPtr(font->Glyphs.Data); + if (font->KerningPairs.Data != 0) + _ = Marshal.ReadIntPtr(font->KerningPairs.Data); + if (font->ConfigDataCount == 0 && font->ConfigData is not null) + throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?"); + if (font->ConfigDataCount != 0 && font->ConfigData is null) + throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?"); + if (font->ConfigData is not null) + _ = Marshal.ReadIntPtr((nint)font->ConfigData); + if (font->FallbackGlyph is not null + && ((nint)font->FallbackGlyph < font->Glyphs.Data || (nint)font->FallbackGlyph >= font->Glyphs.Data)) + throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); + if (font->FallbackHotData is not null + && ((nint)font->FallbackHotData < font->IndexedHotData.Data + || (nint)font->FallbackHotData >= font->IndexedHotData.Data)) + throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); + if (font->ContainerAtlas is not null) + _ = Marshal.ReadIntPtr((nint)font->ContainerAtlas); + } + catch (Exception e) + { + return e; + } + + return null; + } + + /// + /// Updates the fallback char of . + /// + /// The font. + /// The fallback character. + internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c) + { + font.FallbackChar = c; + font.NativePtr->FallbackHotData = + (ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar); + } + + /// + /// Determines if the supplied codepoint is inside the given range, + /// in format of . + /// + /// The codepoint. + /// The ranges. + /// Whether it is the case. + internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr) + { + if (codepoint is <= 0 or >= ushort.MaxValue) + return false; + + while (*rangePtr != 0) + { + var from = *rangePtr++; + var to = *rangePtr++; + if (from <= codepoint && codepoint <= to) + return true; + } + + return false; + } + /// /// Get data needed for each new frame. /// From 0c2a722f83e77e58af3f5cb2b828b8c7aa7f8858 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 19 Jan 2024 08:11:33 +0900 Subject: [PATCH 31/71] Fallback behavior for bad use of DstFont values --- .../FontAtlasFactory.Implementation.cs | 64 ++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 5656fc673..eddccfa76 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -209,7 +209,7 @@ internal sealed partial class FontAtlasFactory private int buildIndex; private bool buildQueued; - private bool disposed = false; + private bool disposed; /// /// Initializes a new instance of the class. @@ -612,6 +612,7 @@ internal sealed partial class FontAtlasFactory var res = default(FontAtlasBuiltData); nint atlasPtr = 0; + BuildToolkit? toolkit = null; try { res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); @@ -627,11 +628,44 @@ internal sealed partial class FontAtlasFactory atlasPtr, sw.ElapsedMilliseconds); - using var toolkit = res.CreateToolkit(this.factory, isAsync); + toolkit = res.CreateToolkit(this.factory, isAsync); this.BuildStepChange?.Invoke(toolkit); toolkit.PreBuildSubstances(); toolkit.PreBuild(); + // Prevent NewImAtlas.ConfigData[].DstFont pointing to a font not owned by the new atlas, + // by making it add a font with default configuration first instead. + if (!ValidateMergeFontReferences(default)) + { + Log.Warning( + "[{name}:{functionname}] 0x{ptr:X}: refering to fonts outside the new atlas; " + + "adding a default font, and using that as the merge target.", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr); + + res.IsBuildInProgress = false; + toolkit.Dispose(); + res.Dispose(); + + res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + unsafe + { + atlasPtr = (nint)res.Atlas.NativePtr; + } + + toolkit = res.CreateToolkit(this.factory, isAsync); + + // PreBuildSubstances deals with toolkit.Add... function family. Do this first. + var defaultFont = toolkit.AddDalamudDefaultFont(InterfaceManager.DefaultFontSizePx, null); + + this.BuildStepChange?.Invoke(toolkit); + toolkit.PreBuildSubstances(); + toolkit.PreBuild(); + + _ = ValidateMergeFontReferences(defaultFont); + } + #if VeryVerboseLog Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); #endif @@ -687,8 +721,34 @@ internal sealed partial class FontAtlasFactory } finally { + toolkit?.Dispose(); this.buildQueued = false; } + + unsafe bool ValidateMergeFontReferences(ImFontPtr replacementDstFont) + { + var correct = true; + foreach (ref var configData in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan) + { + var found = false; + foreach (ref var font in toolkit.Fonts.DataSpan) + { + if (configData.DstFont == font) + { + found = true; + break; + } + } + + if (!found) + { + correct = false; + configData.DstFont = replacementDstFont; + } + } + + return correct; + } } private void OnRebuildRecommend() From ad12045c86b53f698dd611b8ff2a16198fbcdb3f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 19 Jan 2024 08:31:08 +0900 Subject: [PATCH 32/71] Fix notifications font --- Dalamud/Interface/Internal/InterfaceManager.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 3e004727a..94597f3da 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -890,12 +890,13 @@ internal class InterfaceManager : IDisposable, IServiceType if (this.IsDispatchingEvents) { using (this.defaultFontHandle?.Push()) + { this.Draw?.Invoke(); + Service.Get().Draw(); + } } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); - - Service.Get().Draw(); } /// From b415f5a8741f6590ccc3ad64e643b17edeb283ae Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:12:32 +0100 Subject: [PATCH 33/71] never offer updates for dev plugins --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 1545efb65..b2fa50a03 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2246,6 +2246,11 @@ internal class PluginInstallerWindow : Window, IDisposable } var availablePluginUpdate = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin); + + // Dev plugins can never update + if (plugin.IsDev) + availablePluginUpdate = null; + // Update available if (availablePluginUpdate != default) { From d26db7e05342b61e95419827537036c835b886d2 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:26:59 +0100 Subject: [PATCH 34/71] don't tell people to wait for an update, if one is available --- .../PluginInstaller/PluginInstallerWindow.cs | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index b2fa50a03..0c5437724 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1881,16 +1881,32 @@ internal class PluginInstallerWindow : Window, IDisposable if (plugin is { IsOutdated: true, IsBanned: false } || installableOutdated) { ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.TextWrapped(Locs.PluginBody_Outdated); + + var bodyText = Locs.PluginBody_Outdated + " "; + if (updateAvailable) + bodyText += Locs.PluginBody_Outdated_CanNowUpdate; + else + bodyText += Locs.PluginBody_Outdated_WaitForUpdate; + + ImGui.TextWrapped(bodyText); ImGui.PopStyleColor(); } else if (plugin is { IsBanned: true }) { // Banned warning ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGuiHelpers.SafeTextWrapped(plugin.BanReason.IsNullOrEmpty() - ? Locs.PluginBody_Banned - : Locs.PluginBody_BannedReason(plugin.BanReason)); + + var bodyText = plugin.BanReason.IsNullOrEmpty() + ? Locs.PluginBody_Banned + : Locs.PluginBody_BannedReason(plugin.BanReason); + bodyText += " "; + + if (updateAvailable) + bodyText += Locs.PluginBody_Outdated_CanNowUpdate; + else + bodyText += Locs.PluginBody_Outdated_WaitForUpdate; + + ImGuiHelpers.SafeTextWrapped(bodyText); ImGui.PopStyleColor(); } @@ -3497,7 +3513,11 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginBody_Plugin3rdPartyRepo(string url) => Loc.Localize("InstallerPlugin3rdPartyRepo", "From custom plugin repository {0}").Format(url); - public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible at the moment. Please wait for it to be updated by its author."); + public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible."); + + public static string PluginBody_Outdated_WaitForUpdate => Loc.Localize("InstallerOutdatedWaitForUpdate", "Please wait for it to be updated by its author."); + + public static string PluginBody_Outdated_CanNowUpdate => Loc.Localize("InstallerOutdatedCanNowUpdate", "An update is available for installation."); public static string PluginBody_Orphaned => Loc.Localize("InstallerOrphanedPluginBody ", "This plugin's source repository is no longer available. You may need to reinstall it from its repository, or re-add the repository."); @@ -3507,7 +3527,7 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginBody_LoadFailed => Loc.Localize("InstallerLoadFailedPluginBody ", "This plugin failed to load. Please contact the author for more information."); - public static string PluginBody_Banned => Loc.Localize("InstallerBannedPluginBody ", "This plugin was automatically disabled due to incompatibilities and is not available at the moment. Please wait for it to be updated by its author."); + public static string PluginBody_Banned => Loc.Localize("InstallerBannedPluginBody ", "This plugin was automatically disabled due to incompatibilities and is not available."); public static string PluginBody_Policy => Loc.Localize("InstallerPolicyPluginBody ", "Plugin loads for this type of plugin were manually disabled."); From 4e95d4fe37e811b031538b2469cdfb060c82e69d Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:32:39 +0100 Subject: [PATCH 35/71] allow load of devPlugins in non-default profile --- Dalamud/Plugin/Internal/PluginManager.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index c57487d1d..b0a421b0d 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1506,6 +1506,16 @@ internal partial class PluginManager : IDisposable, IServiceType { // We don't know about this plugin, so we don't want to do anything here. // The code below will take care of it and add it with the default value. + Log.Verbose("DevPlugin {Name} not wanted in default plugin", plugin.Manifest.InternalName); + + // If it is wanted by any other plugin, we do want to load it. This means we are looking it up twice, but I don't care right now. + // I am putting a TODO so that goat will clean it up some day soon. + if (await this.profileManager.GetWantStateAsync( + plugin.Manifest.WorkingPluginId, + plugin.Manifest.InternalName, + false, + false)) + loadPlugin = true; } else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot) { @@ -1544,19 +1554,20 @@ internal partial class PluginManager : IDisposable, IServiceType #pragma warning restore CS0618 // Need to do this here, so plugins that don't load are still added to the default profile - var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); - + var wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); + Log.Information("{Name} defaultState: {State} wantedByAnyProfile: {WantedByAny} loadPlugin: {LoadPlugin}", plugin.Manifest.InternalName, defaultState, wantedByAnyProfile, loadPlugin); + if (loadPlugin) { try { - if (wantToLoad && !plugin.IsOrphaned) + if (wantedByAnyProfile && !plugin.IsOrphaned) { await plugin.LoadAsync(reason); } else { - Log.Verbose($"{name} not loaded, wantToLoad:{wantToLoad} orphaned:{plugin.IsOrphaned}"); + Log.Verbose($"{name} not loaded, wantToLoad:{wantedByAnyProfile} orphaned:{plugin.IsOrphaned}"); } } catch (InvalidPluginException) From 57b8a5d932b0449acd4fe840c6d6c601cbfcf56d Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:42:44 +0100 Subject: [PATCH 36/71] prevent double-lookup for dev plugins in non-default profiles --- Dalamud/Plugin/Internal/PluginManager.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index b0a421b0d..8bfb38c34 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1493,6 +1493,8 @@ internal partial class PluginManager : IDisposable, IServiceType if (plugin.Manifest.WorkingPluginId == Guid.Empty) throw new Exception("Plugin should have a WorkingPluginId at this point"); this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.Manifest.WorkingPluginId); + + var wantedByAnyProfile = false; // Now, if this is a devPlugin, figure out if we want to load it if (isDev) @@ -1508,13 +1510,12 @@ internal partial class PluginManager : IDisposable, IServiceType // The code below will take care of it and add it with the default value. Log.Verbose("DevPlugin {Name} not wanted in default plugin", plugin.Manifest.InternalName); - // If it is wanted by any other plugin, we do want to load it. This means we are looking it up twice, but I don't care right now. - // I am putting a TODO so that goat will clean it up some day soon. - if (await this.profileManager.GetWantStateAsync( - plugin.Manifest.WorkingPluginId, - plugin.Manifest.InternalName, - false, - false)) + // Check if any profile wants this plugin. We need to do this here, since we want to allow loading a dev plugin if a non-default profile wants it active. + // Note that this will not add the plugin to the default profile. That's done below in any other case. + wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); + + // If it is wanted by any other profile, we do want to load it. + if (wantedByAnyProfile) loadPlugin = true; } else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot) @@ -1553,8 +1554,9 @@ internal partial class PluginManager : IDisposable, IServiceType var defaultState = manifest?.Disabled != true && loadPlugin; #pragma warning restore CS0618 - // Need to do this here, so plugins that don't load are still added to the default profile - var wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); + // Plugins that aren't in any profile will be added to the default profile with this call. + // We are skipping a double-lookup for dev plugins that are wanted by non-default profiles, as noted above. + wantedByAnyProfile = wantedByAnyProfile || await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); Log.Information("{Name} defaultState: {State} wantedByAnyProfile: {WantedByAny} loadPlugin: {LoadPlugin}", plugin.Manifest.InternalName, defaultState, wantedByAnyProfile, loadPlugin); if (loadPlugin) From af2f0f290f0a80926740d17cde0eaf190e8ca2c0 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:43:24 +0100 Subject: [PATCH 37/71] dev plugins are now allowed to be in profiles --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0c5437724..0d1a07769 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2555,7 +2555,7 @@ internal class PluginInstallerWindow : Window, IDisposable var profileManager = Service.Get(); var config = Service.Get(); - var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev; + var applicableForProfiles = plugin.Manifest.SupportsProfiles /*&& !plugin.IsDev*/; var profilesThatWantThisPlugin = profileManager.Profiles .Where(x => x.WantsPlugin(plugin.Manifest.WorkingPluginId) != null) .ToArray(); From 4f4f604ef87bece0c4cb113ac74150fbf09e01b4 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 20 Jan 2024 01:10:07 +0100 Subject: [PATCH 38/71] show all plugins - be it dev, installed, available, orphaned - in the available tab --- .../PluginInstaller/PluginInstallerWindow.cs | 89 +++++++++++++------ .../PluginInstaller/ProfileManagerWidget.cs | 6 +- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0d1a07769..5007691ab 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -107,6 +107,7 @@ internal class PluginInstallerWindow : Window, IDisposable private int updatePluginCount = 0; private List? updatedPlugins; + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Makes sense like this")] private List pluginListAvailable = new(); private List pluginListInstalled = new(); private List pluginListUpdatable = new(); @@ -1126,45 +1127,79 @@ internal class PluginInstallerWindow : Window, IDisposable this.DrawChangelog(logEntry); } } - + + private record PluginInstallerAvailablePluginProxy(RemotePluginManifest? RemoteManifest, LocalPlugin? LocalPlugin); + +#pragma warning disable SA1201 private void DrawAvailablePluginList() +#pragma warning restore SA1201 { - var pluginList = this.pluginListAvailable; + var availableManifests = this.pluginListAvailable; + var installedPlugins = this.pluginListInstalled.ToList(); // Copy intended - if (pluginList.Count == 0) + if (availableManifests.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoCompatible); return; } - var filteredManifests = pluginList + var filteredAvailableManifests = availableManifests .Where(rm => !this.IsManifestFiltered(rm)) .ToList(); - if (filteredManifests.Count == 0) + if (filteredAvailableManifests.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching); return; } - // get list to show and reset category dirty flag - var categoryManifestsList = this.categoryManager.GetCurrentCategoryContent(filteredManifests); + var proxies = new List(); + + // Go through all AVAILABLE manifests, associate them with a NON-DEV local plugin, if one is available, and remove it from the pile + foreach (var availableManifest in this.categoryManager.GetCurrentCategoryContent(filteredAvailableManifests).Cast()) + { + var plugin = this.pluginListInstalled.FirstOrDefault(plugin => plugin.Manifest.InternalName == availableManifest.InternalName && plugin.Manifest.RepoUrl == availableManifest.RepoUrl); + + // We "consumed" this plugin from the pile and remove it. + if (plugin != null && !plugin.IsDev) + { + installedPlugins.Remove(plugin); + proxies.Add(new PluginInstallerAvailablePluginProxy(null, plugin)); + + continue; + } + + proxies.Add(new PluginInstallerAvailablePluginProxy(availableManifest, null)); + } + + // Now, add all applicable local plugins that haven't been "used up", in most cases either dev or orphaned plugins. + foreach (var installedPlugin in installedPlugins) + { + if (this.IsManifestFiltered(installedPlugin.Manifest)) + continue; + + // TODO: We should also check categories here, for good measure + + proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin)); + } var i = 0; - foreach (var manifest in categoryManifestsList) + foreach (var proxy in proxies) { - if (manifest is not RemotePluginManifest remoteManifest) - continue; - var (isInstalled, plugin) = this.IsManifestInstalled(remoteManifest); + IPluginManifest applicableManifest = proxy.LocalPlugin != null ? proxy.LocalPlugin.Manifest : proxy.RemoteManifest; - ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}"); - if (isInstalled) + if (applicableManifest == null) + throw new Exception("Could not determine manifest for available plugin"); + + ImGui.PushID($"{applicableManifest.InternalName}{applicableManifest.AssemblyVersion}"); + + if (proxy.LocalPlugin != null) { - this.DrawInstalledPlugin(plugin, i++, true); + this.DrawInstalledPlugin(proxy.LocalPlugin, i++, true); } - else + else if (proxy.RemoteManifest != null) { - this.DrawAvailablePlugin(remoteManifest, i++); + this.DrawAvailablePlugin(proxy.RemoteManifest, i++); } ImGui.PopID(); @@ -1800,14 +1835,6 @@ internal class PluginInstallerWindow : Window, IDisposable var isLoaded = plugin is { IsLoaded: true }; - if (plugin is LocalDevPlugin) - { - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); - ImGui.Image(this.imageCache.DevPluginIcon.ImGuiHandle, iconSize); - ImGui.PopStyleVar(); - ImGui.SetCursorPos(cursorBeforeImage); - } - if (updateAvailable) ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); else if ((trouble && !pluginDisabled) || isOrphan) @@ -1836,8 +1863,7 @@ internal class PluginInstallerWindow : Window, IDisposable // Name ImGui.TextUnformatted(label); - // Verified Checkmark, don't show for dev plugins - if (plugin is null or { IsDev: false }) + // Verified Checkmark or dev plugin wrench { ImGui.SameLine(); ImGui.Text(" "); @@ -1847,8 +1873,15 @@ internal class PluginInstallerWindow : Window, IDisposable var unverifiedOutlineColor = KnownColor.Black.Vector(); var verifiedIconColor = KnownColor.RoyalBlue.Vector() with { W = 0.75f }; var unverifiedIconColor = KnownColor.Orange.Vector(); - - if (!isThirdParty) + var devIconOutlineColor = KnownColor.White.Vector(); + var devIconColor = KnownColor.MediumOrchid.Vector(); + + if (plugin is LocalDevPlugin) + { + this.DrawFontawesomeIconOutlined(FontAwesomeIcon.Wrench, devIconOutlineColor, devIconColor); + this.VerifiedCheckmarkFadeTooltip(label, "This is a dev plugin. You added it."); + } + else if (!isThirdParty) { this.DrawFontawesomeIconOutlined(FontAwesomeIcon.CheckCircle, verifiedOutlineColor, verifiedIconColor); this.VerifiedCheckmarkFadeTooltip(label, Locs.VerifiedCheckmark_VerifiedTooltip); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 2d45869e0..eafea9d16 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -441,17 +441,17 @@ internal class ProfileManagerWidget ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight)); - if (pmPlugin is LocalDevPlugin) + if (pmPlugin.IsDev) { ImGui.SetCursorPos(cursorBeforeIcon); - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.7f); ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight)); ImGui.PopStyleVar(); } ImGui.SameLine(); - var text = $"{pmPlugin.Name}"; + var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}"; var textHeight = ImGui.CalcTextSize(text); var before = ImGui.GetCursorPos(); From 1a19cbf277a3e8d2a0dc079de4c12641bb9f7da2 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 20 Jan 2024 10:21:50 +0900 Subject: [PATCH 39/71] Set ImGui default font upon default font atlas update --- Dalamud/Interface/Internal/InterfaceManager.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 94597f3da..8915b3e3d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -717,6 +717,12 @@ internal class InterfaceManager : IDisposable, IServiceType // Fill missing glyphs in MonoFont from DefaultFont tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); + // Update default font + unsafe + { + ImGui.GetIO().NativePtr->FontDefault = this.defaultFontHandle.ImFont; + } + // Broadcast to auto-rebuilding instances this.AfterBuildFonts?.Invoke(); }); @@ -889,11 +895,8 @@ internal class InterfaceManager : IDisposable, IServiceType if (this.IsDispatchingEvents) { - using (this.defaultFontHandle?.Push()) - { - this.Draw?.Invoke(); - Service.Get().Draw(); - } + this.Draw?.Invoke(); + Service.Get().Draw(); } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); From dd5cbdfd5daacadde9fbbf2b42a72af2b7c9dbcb Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 00:15:12 +0900 Subject: [PATCH 40/71] IFontAtlas API9 compat: support reading GameFontHandle.ImFont during UiBuilder.After/BuildFonts --- .../IFontAtlasBuildToolkit.cs | 16 +++++++ .../Internals/DelegateFontHandle.cs | 9 ++++ .../FontAtlasFactory.BuildToolkit.cs | 30 ++++++++++++- .../Internals/GamePrebakedFontHandle.cs | 32 ++++++++++++-- .../Internals/IFontHandleSubstance.cs | 17 ++++++- Dalamud/Interface/UiBuilder.cs | 44 ++++++++++++------- Dalamud/Logging/PluginLog.cs | 3 ++ Dalamud/Utility/Api10ToDoAttribute.cs | 19 ++++++++ 8 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 Dalamud/Utility/Api10ToDoAttribute.cs diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs index 4b016bbb2..a997c48c1 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -1,6 +1,8 @@ using System.Runtime.InteropServices; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; +using Dalamud.Utility; using ImGuiNET; @@ -11,6 +13,20 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// public interface IFontAtlasBuildToolkit { + /// + /// Functionalities for compatibility behavior.
+ ///
+ [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + internal interface IApi9Compat : IFontAtlasBuildToolkit + { + /// + /// Invokes , temporarily applying s.
+ ///
+ /// The action to invoke. + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public void FromUiBuilderObsoleteEventHandlers(Action action); + } + /// /// Gets or sets the font relevant to the call. /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index f0ed09155..99067a9de 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Logging.Internal; +using Dalamud.Utility; using ImGuiNET; @@ -144,6 +145,14 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public IFontHandleManager Manager { get; } + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public IFontAtlasBuildToolkitPreBuild? PreBuildToolkitForApi9Compat { get; set; } + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public bool CreateFontOnAccess { get; set; } + /// public void Dispose() { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index e73ea7548..fde115c9e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -30,7 +30,7 @@ internal sealed partial class FontAtlasFactory /// Implementations for and /// . ///
- private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable + private class BuildToolkit : IFontAtlasBuildToolkit.IApi9Compat, IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable { private static readonly ushort FontAwesomeIconMin = (ushort)Enum.GetValues().Where(x => x > 0).Min(); @@ -107,6 +107,34 @@ internal sealed partial class FontAtlasFactory /// public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action); + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public void FromUiBuilderObsoleteEventHandlers(Action action) + { + var previousSubstances = new IFontHandleSubstance[this.data.Substances.Count]; + for (var i = 0; i < previousSubstances.Length; i++) + { + previousSubstances[i] = this.data.Substances[i].Manager.Substance; + this.data.Substances[i].Manager.Substance = this.data.Substances[i]; + this.data.Substances[i].CreateFontOnAccess = true; + this.data.Substances[i].PreBuildToolkitForApi9Compat = this; + } + + try + { + action(); + } + finally + { + for (var i = 0; i < previousSubstances.Length; i++) + { + this.data.Substances[i].Manager.Substance = previousSubstances[i]; + this.data.Substances[i].CreateFontOnAccess = false; + this.data.Substances[i].PreBuildToolkitForApi9Compat = null; + } + } + } + /// public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 99c817a91..2686259bc 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -230,6 +230,14 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public IFontHandleManager Manager => this.handleManager; + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public IFontAtlasBuildToolkitPreBuild? PreBuildToolkitForApi9Compat { get; set; } + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public bool CreateFontOnAccess { get; set; } + /// public void Dispose() { @@ -285,11 +293,27 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } + // Use this on API 10. + // /// + // public ImFontPtr GetFontPtr(IFontHandle handle) => + // handle is GamePrebakedFontHandle ggfh + // ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default + // : default; + /// - public ImFontPtr GetFontPtr(IFontHandle handle) => - handle is GamePrebakedFontHandle ggfh - ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default - : default; + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public ImFontPtr GetFontPtr(IFontHandle handle) + { + if (handle is not GamePrebakedFontHandle ggfh) + return default; + if (this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont is { } font) + return font; + if (!this.CreateFontOnAccess) + return default; + if (this.PreBuildToolkitForApi9Compat is not { } tk) + return default; + return this.GetOrCreateFont(ggfh.FontStyle, tk); + } /// public Exception? GetBuildException(IFontHandle handle) => diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs index f6c5c6591..c800c30ac 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -1,4 +1,6 @@ -using ImGuiNET; +using Dalamud.Utility; + +using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -12,6 +14,19 @@ internal interface IFontHandleSubstance : IDisposable ///
IFontHandleManager Manager { get; } + /// + /// Gets or sets the relevant for this. + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + IFontAtlasBuildToolkitPreBuild? PreBuildToolkitForApi9Compat { get; set; } + + /// + /// Gets or sets a value indicating whether to create a new instance of on first + /// access, for compatibility with API 9. + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + bool CreateFontOnAccess { get; set; } + /// /// Gets the font. /// diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index a477ec09e..87e3b9032 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -104,6 +104,7 @@ public sealed class UiBuilder : IDisposable /// pointers inside this handler. ///
[Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? BuildFonts; /// @@ -113,6 +114,7 @@ public sealed class UiBuilder : IDisposable /// pointers inside this handler. /// [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? AfterBuildFonts; ///
@@ -423,6 +425,7 @@ public sealed class UiBuilder : IDisposable /// Font to get. /// Handle to the game font which may or may not be available for use yet. [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), Service.Get()); @@ -620,28 +623,37 @@ public sealed class UiBuilder : IDisposable this.hitchDetector.Stop(); } + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e) { if (e.IsAsyncBuildOperation) return; - e.OnPreBuild( - _ => - { - var prev = ImGui.GetIO().NativePtr->Fonts; - ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; - this.BuildFonts?.InvokeSafely(); - ImGui.GetIO().NativePtr->Fonts = prev; - }); + if (this.BuildFonts is not null) + { + e.OnPreBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + ((IFontAtlasBuildToolkit.IApi9Compat)e) + .FromUiBuilderObsoleteEventHandlers(() => this.BuildFonts?.InvokeSafely()); + ImGui.GetIO().NativePtr->Fonts = prev; + }); + } - e.OnPostBuild( - _ => - { - var prev = ImGui.GetIO().NativePtr->Fonts; - ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; - this.AfterBuildFonts?.InvokeSafely(); - ImGui.GetIO().NativePtr->Fonts = prev; - }); + if (this.AfterBuildFonts is not null) + { + e.OnPostBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + ((IFontAtlasBuildToolkit.IApi9Compat)e) + .FromUiBuilderObsoleteEventHandlers(() => this.AfterBuildFonts?.InvokeSafely()); + ImGui.GetIO().NativePtr->Fonts = prev; + }); + } } private void OnResizeBuffers() diff --git a/Dalamud/Logging/PluginLog.cs b/Dalamud/Logging/PluginLog.cs index decf10b4c..e3744c617 100644 --- a/Dalamud/Logging/PluginLog.cs +++ b/Dalamud/Logging/PluginLog.cs @@ -1,6 +1,8 @@ using System.Reflection; using Dalamud.Plugin.Services; +using Dalamud.Utility; + using Serilog; using Serilog.Events; @@ -14,6 +16,7 @@ namespace Dalamud.Logging; /// move over as soon as reasonably possible for performance reasons. /// [Obsolete("Static PluginLog will be removed in API 10. Developers should use IPluginLog.")] +[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public static class PluginLog { #region "Log" prefixed Serilog style methods diff --git a/Dalamud/Utility/Api10ToDoAttribute.cs b/Dalamud/Utility/Api10ToDoAttribute.cs new file mode 100644 index 000000000..f397f8f0c --- /dev/null +++ b/Dalamud/Utility/Api10ToDoAttribute.cs @@ -0,0 +1,19 @@ +namespace Dalamud.Utility; + +/// +/// Utility class for marking something to be changed for API 10, for ease of lookup. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +internal sealed class Api10ToDoAttribute : Attribute +{ + /// + /// Marks that this exists purely for making API 9 plugins work. + /// + public const string DeleteCompatBehavior = "Delete. This is for making API 9 plugins work."; + + /// + /// Initializes a new instance of the class. + /// + /// The explanation. + public Api10ToDoAttribute(string what) => _ = what; +} From 8afe277c0218b8f54c5250550009edc91b9a0040 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 00:45:46 +0900 Subject: [PATCH 41/71] Make IFontHandle.Pop return a concrete struct --- Dalamud/Interface/GameFonts/GameFontHandle.cs | 10 ++++- .../Interface/ManagedFontAtlas/IFontHandle.cs | 40 ++++++++++++++++++- .../Internals/DelegateFontHandle.cs | 3 +- .../Internals/GamePrebakedFontHandle.cs | 3 +- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 77461aa0a..d11414517 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -2,6 +2,7 @@ using System.Numerics; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Utility; using ImGuiNET; @@ -10,6 +11,7 @@ namespace Dalamud.Interface.GameFonts; /// /// ABI-compatible wrapper for . /// +[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public sealed class GameFontHandle : IFontHandle { private readonly IFontHandle.IInternal fontHandle; @@ -53,8 +55,14 @@ public sealed class GameFontHandle : IFontHandle /// public void Dispose() => this.fontHandle.Dispose(); - /// + /// + /// Pushes the font. + /// + /// An that can be used to pop the font on dispose. public IDisposable Push() => this.fontHandle.Push(); + + /// + IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push(); /// /// Creates a new .
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 854594663..47f384c11 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -1,4 +1,6 @@ -using ImGuiNET; +using Dalamud.Utility; + +using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas; @@ -38,5 +40,39 @@ public interface IFontHandle : IDisposable /// You may not access the font once you dispose this object. ///
/// A disposable object that will call (1) on dispose. - IDisposable Push(); + /// If called outside of the main thread. + FontPopper Push(); + + /// + /// The wrapper for popping fonts. + /// + public struct FontPopper : IDisposable + { + private int count; + + /// + /// Initializes a new instance of the struct. + /// + /// The font to push. + /// Whether to push. + internal FontPopper(ImFontPtr fontPtr, bool push) + { + if (!push) + return; + + ThreadSafety.AssertMainThread(); + + this.count = 1; + ImGui.PushFont(fontPtr); + } + + /// + public void Dispose() + { + ThreadSafety.AssertMainThread(); + + while (this.count-- > 0) + ImGui.PopFont(); + } + } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index 99067a9de..bde349736 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -2,7 +2,6 @@ using System.Linq; using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; using Dalamud.Logging.Internal; using Dalamud.Utility; @@ -53,7 +52,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal } /// - public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); ///
/// Manager for s. diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 2686259bc..feda47a8a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -9,7 +9,6 @@ using Dalamud.Game.Text; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using ImGuiNET; @@ -117,7 +116,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } /// - public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); /// /// Manager for s. From 7c1ca4001d0b4787638cd065deb25ccffe2f7e27 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 00:47:09 +0900 Subject: [PATCH 42/71] Docs --- Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 47f384c11..460fd53a0 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -41,6 +41,10 @@ public interface IFontHandle : IDisposable /// /// A disposable object that will call (1) on dispose. /// If called outside of the main thread. + /// + /// Only intended for use with using keywords, such as using (handle.Push()).
+ /// Should you store or transfer the return value to somewhere else, use as the type. + ///
FontPopper Push(); /// From d70b430e0dd19b934b74c39591cd3c504747b6f0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 03:10:41 +0900 Subject: [PATCH 43/71] Add IFontHandle.Lock and WaitAsync --- Dalamud/Interface/GameFonts/GameFontHandle.cs | 16 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 53 ++++++ .../Interface/ManagedFontAtlas/IFontHandle.cs | 80 ++++++++- .../Internals/DelegateFontHandle.cs | 118 ++++++++++++-- .../FontAtlasFactory.Implementation.cs | 153 +++++++++++------- .../Internals/GamePrebakedFontHandle.cs | 117 +++++++++++++- .../Internals/IFontHandleManager.cs | 10 +- .../Internals/IFontHandleSubstance.cs | 5 + Dalamud/Utility/IRefCountable.cs | 77 +++++++++ 9 files changed, 543 insertions(+), 86 deletions(-) create mode 100644 Dalamud/Utility/IRefCountable.cs diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index d11414517..6591ce0fe 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Threading.Tasks; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; @@ -28,6 +29,13 @@ public sealed class GameFontHandle : IFontHandle this.fontAtlasFactory = fontAtlasFactory; } + /// + public event Action ImFontChanged + { + add => this.fontHandle.ImFontChanged += value; + remove => this.fontHandle.ImFontChanged -= value; + } + /// public Exception? LoadException => this.fontHandle.LoadException; @@ -55,15 +63,21 @@ public sealed class GameFontHandle : IFontHandle /// public void Dispose() => this.fontHandle.Dispose(); + /// + public IFontHandle.ImFontLocked Lock() => this.fontHandle.Lock(); + /// /// Pushes the font. /// /// An that can be used to pop the font on dispose. public IDisposable Push() => this.fontHandle.Push(); - + /// IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push(); + /// + public Task WaitAsync() => this.fontHandle.WaitAsync(); + /// /// Creates a new .
///
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index dba293e8b..b3b57343c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Numerics; using System.Text; +using System.Threading.Tasks; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; @@ -11,6 +13,8 @@ using Dalamud.Utility; using ImGuiNET; +using Serilog; + namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// @@ -103,6 +107,10 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable minCapacity: 1024); } + ImGui.SameLine(); + if (ImGui.Button("Test Lock")) + Task.Run(this.TestLock); + fixed (byte* labelPtr = "Test Input"u8) { if (ImGuiNative.igInputTextMultiline( @@ -210,4 +218,49 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable this.privateAtlas?.Dispose(); this.privateAtlas = null; } + + private async void TestLock() + { + if (this.fontHandles is not { } fontHandlesCopy) + return; + + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} waiting for build"); + + await using var garbage = new DisposeSafety.ScopedFinalizer(); + var fonts = new List(); + IFontHandle[] handles; + try + { + handles = fontHandlesCopy.Values.SelectMany(x => x).Select(x => x.Handle.Value).ToArray(); + foreach (var handle in handles) + { + await handle.WaitAsync(); + var locked = handle.Lock(); + garbage.Add(locked); + fonts.Add(locked); + } + } + catch (ObjectDisposedException) + { + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} cancelled"); + return; + } + + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} waiting in lock"); + await Task.Delay(5000); + + foreach (var (font, handle) in fonts.Zip(handles)) + TestSingle(font, handle); + + return; + + unsafe void TestSingle(ImFontPtr fontPtr, IFontHandle handle) + { + var dim = default(Vector2); + var test = "Test string"u8; + fixed (byte* pTest = test) + ImGuiNative.ImFont_CalcTextSizeA(&dim, fontPtr, fontPtr.FontSize, float.MaxValue, 0, pTest, null, null); + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {handle} => {dim}"); + } + } } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 460fd53a0..81ce84a63 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -1,4 +1,6 @@ -using Dalamud.Utility; +using System.Threading.Tasks; + +using Dalamud.Utility; using ImGuiNET; @@ -9,6 +11,11 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// public interface IFontHandle : IDisposable { + /// + /// Called when the built instance of has been changed. + /// + event Action ImFontChanged; + /// /// Represents a reference counting handle for fonts. Dalamud internal use only. /// @@ -18,7 +25,8 @@ public interface IFontHandle : IDisposable /// Gets the font.
/// Use of this properly is safe only from the UI thread.
/// Use if the intended purpose of this property is .
- /// Futures changes may make simple not enough. + /// Futures changes may make simple not enough.
+ /// If you need to access a font outside the UI thread, consider using . ///
ImFontPtr ImFont { get; } } @@ -29,11 +37,27 @@ public interface IFontHandle : IDisposable Exception? LoadException { get; } /// - /// Gets a value indicating whether this font is ready for use.
- /// Use directly if you want to keep the current ImGui font if the font is not ready. + /// Gets a value indicating whether this font is ready for use. ///
+ /// + /// Once set to true, it will remain true.
+ /// Use directly if you want to keep the current ImGui font if the font is not ready.
+ /// Alternatively, use to wait for this property to become true. + ///
bool Available { get; } + /// + /// Locks the fully constructed instance of corresponding to the this + /// , for read-only use in any thread. + /// + /// An instance of that must be disposed after use. + /// + /// Calling . will not unlock the + /// locked by this function. + /// + /// If is false. + ImFontLocked Lock(); + /// /// Pushes the current font into ImGui font stack using , if available.
/// Use to access the current font.
@@ -47,6 +71,54 @@ public interface IFontHandle : IDisposable /// FontPopper Push(); + /// + /// Waits for to become true. + /// + /// A task containing this . + Task WaitAsync(); + + /// + /// The wrapper for , guaranteeing that the associated data will be available as long as + /// this struct is not disposed. + /// + public struct ImFontLocked : IDisposable + { + /// + /// The associated . + /// + public ImFontPtr ImFont; + + private IRefCountable? owner; + + /// + /// Initializes a new instance of the struct, + /// and incrase the reference count of . + /// + /// The contained font. + /// The owner. + internal ImFontLocked(ImFontPtr imFont, IRefCountable owner) + { + owner.AddRef(); + this.ImFont = imFont; + this.owner = owner; + } + + public static implicit operator ImFontPtr(ImFontLocked l) => l.ImFont; + + public static unsafe implicit operator ImFont*(ImFontLocked l) => l.ImFont.NativePtr; + + /// + public void Dispose() + { + if (this.owner is null) + return; + + this.owner.Release(); + this.owner = null; + this.ImFont = default; + } + } + /// /// The wrapper for popping fonts. /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index bde349736..f50967fae 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -27,6 +28,11 @@ internal class DelegateFontHandle : IFontHandle.IInternal this.CallOnBuildStepChange = callOnBuildStepChange; } + /// + public event Action? ImFontChanged; + + private event Action? Disposed; + /// /// Gets the function to be called on build step changes. /// @@ -49,11 +55,76 @@ internal class DelegateFontHandle : IFontHandle.IInternal { this.manager?.FreeFontHandle(this); this.manager = null; + this.Disposed?.InvokeSafely(this); + this.ImFontChanged = null; + } + + /// + public IFontHandle.ImFontLocked Lock() + { + IFontHandleSubstance? prevSubstance = default; + while (true) + { + var substance = this.ManagerNotDisposed.Substance; + if (substance is null) + throw new InvalidOperationException(); + if (substance == prevSubstance) + throw new ObjectDisposedException(nameof(DelegateFontHandle)); + + prevSubstance = substance; + try + { + substance.DataRoot.AddRef(); + } + catch (ObjectDisposedException) + { + continue; + } + + try + { + var fontPtr = substance.GetFontPtr(this); + if (fontPtr.IsNull()) + continue; + return new(fontPtr, substance.DataRoot); + } + finally + { + substance.DataRoot.Release(); + } + } } /// public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + /// + public Task WaitAsync() + { + if (this.Available) + return Task.FromResult(this); + + var tcs = new TaskCompletionSource(); + this.ImFontChanged += OnImFontChanged; + this.Disposed += OnImFontChanged; + if (this.Available) + OnImFontChanged(this); + return tcs.Task; + + void OnImFontChanged(IFontHandle unused) + { + if (tcs.Task.IsCompletedSuccessfully) + return; + + this.ImFontChanged -= OnImFontChanged; + this.Disposed -= OnImFontChanged; + if (this.manager is null) + tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); + else + tcs.SetResult(this); + } + } + /// /// Manager for s. /// @@ -81,11 +152,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal public void Dispose() { lock (this.syncRoot) - { this.handles.Clear(); - this.Substance?.Dispose(); - this.Substance = null; - } } /// @@ -109,10 +176,20 @@ internal class DelegateFontHandle : IFontHandle.IInternal } /// - public IFontHandleSubstance NewSubstance() + public void InvokeFontHandleImFontChanged() + { + if (this.Substance is not HandleSubstance hs) + return; + + foreach (var handle in hs.RelevantHandles) + handle.ImFontChanged?.InvokeSafely(handle); + } + + /// + public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { lock (this.syncRoot) - return new HandleSubstance(this, this.handles.ToArray()); + return new HandleSubstance(this, dataRoot, this.handles.ToArray()); } } @@ -123,9 +200,6 @@ internal class DelegateFontHandle : IFontHandle.IInternal { private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); - // Not owned by this class. Do not dispose. - private readonly DelegateFontHandle[] relevantHandles; - // Owned by this class, but ImFontPtr values still do not belong to this. private readonly Dictionary fonts = new(); private readonly Dictionary buildExceptions = new(); @@ -134,13 +208,29 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// Initializes a new instance of the class. ///
/// The manager. + /// The data root. /// The relevant handles. - public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) + public HandleSubstance( + IFontHandleManager manager, + IRefCountable dataRoot, + DelegateFontHandle[] relevantHandles) { + // We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot. + this.Manager = manager; - this.relevantHandles = relevantHandles; + this.DataRoot = dataRoot; + this.RelevantHandles = relevantHandles; } + /// + /// Gets the relevant handles. + /// + // Not owned by this class. Do not dispose. + public DelegateFontHandle[] RelevantHandles { get; } + + /// + public IRefCountable DataRoot { get; } + /// public IFontHandleManager Manager { get; } @@ -171,7 +261,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) { var fontsVector = toolkitPreBuild.Fonts; - foreach (var k in this.relevantHandles) + foreach (var k in this.RelevantHandles) { var fontCountPrevious = fontsVector.Length; @@ -288,7 +378,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) { - foreach (var k in this.relevantHandles) + foreach (var k in this.RelevantHandles) { if (!this.fonts[k].IsNotNullAndLoaded()) continue; @@ -315,7 +405,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) { - foreach (var k in this.relevantHandles) + foreach (var k in this.RelevantHandles) { if (!this.fonts[k].IsNotNullAndLoaded()) continue; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index eddccfa76..99ce8dab9 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -43,68 +43,67 @@ internal sealed partial class FontAtlasFactory private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); - private struct FontAtlasBuiltData : IDisposable + private class FontAtlasBuiltData : IRefCountable { - public readonly DalamudFontAtlas? Owner; - public readonly ImFontAtlasPtr Atlas; - public readonly float Scale; + private readonly List wraps; + private readonly List substances; - public bool IsBuildInProgress; + private int refCount; - private readonly List? wraps; - private readonly List? substances; - private readonly DisposeSafety.ScopedFinalizer? garbage; - - public unsafe FontAtlasBuiltData( - DalamudFontAtlas owner, - IEnumerable substances, - float scale) + public unsafe FontAtlasBuiltData(DalamudFontAtlas owner, float scale) { this.Owner = owner; this.Scale = scale; - this.garbage = new(); + this.Garbage = new(); + this.refCount = 1; try { var substancesList = this.substances = new(); - foreach (var s in substances) - substancesList.Add(this.garbage.Add(s)); - this.garbage.Add(() => substancesList.Clear()); + this.Garbage.Add(() => substancesList.Clear()); var wrapsCopy = this.wraps = new(); - this.garbage.Add(() => wrapsCopy.Clear()); + this.Garbage.Add(() => wrapsCopy.Clear()); var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas(); this.Atlas = atlasPtr; if (this.Atlas.NativePtr is null) throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}."); - this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); + this.Garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); this.IsBuildInProgress = true; } catch { - this.garbage.Dispose(); + this.Garbage.Dispose(); throw; } } - public readonly DisposeSafety.ScopedFinalizer Garbage => - this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + public DalamudFontAtlas? Owner { get; } - public readonly ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); + public ImFontAtlasPtr Atlas { get; } - public readonly ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + public float Scale { get; } - public readonly ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + public bool IsBuildInProgress { get; set; } - public readonly IReadOnlyList Wraps => - (IReadOnlyList?)this.wraps ?? Array.Empty(); + public DisposeSafety.ScopedFinalizer Garbage { get; } - public readonly IReadOnlyList Substances => - (IReadOnlyList?)this.substances ?? Array.Empty(); + public ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); - public readonly void AddExistingTexture(IDalamudTextureWrap wrap) + public ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + + public ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + + public IReadOnlyList Wraps => this.wraps; + + public IReadOnlyList Substances => this.substances; + + public void InitialAddSubstance(IFontHandleSubstance substance) => + this.substances.Add(this.Garbage.Add(substance)); + + public void AddExistingTexture(IDalamudTextureWrap wrap) { if (this.wraps is null) throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); @@ -112,7 +111,7 @@ internal sealed partial class FontAtlasFactory this.wraps.Add(this.Garbage.Add(wrap)); } - public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) + public int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) { if (this.wraps is null) throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); @@ -160,27 +159,47 @@ internal sealed partial class FontAtlasFactory return index; } - public unsafe void Dispose() + public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch { - if (this.garbage is null) - return; + IRefCountable.RefCountResult.StillAlive => newRefCount, + IRefCountable.RefCountResult.AlreadyDisposed => + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)), + IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(), + _ => throw new InvalidOperationException(), + }; - if (this.IsBuildInProgress) + public unsafe int Release() + { + switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount)) { - Log.Error( - "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + - "Stack:\n{trace}", - this.Owner?.Name ?? "", - (nint)this.Atlas.NativePtr, - new StackTrace()); - while (this.IsBuildInProgress) - Thread.Sleep(100); - } + case IRefCountable.RefCountResult.StillAlive: + return newRefCount; + + case IRefCountable.RefCountResult.FinalRelease: + if (this.IsBuildInProgress) + { + Log.Error( + "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + + "Stack:\n{trace}", + this.Owner?.Name ?? "", + (nint)this.Atlas.NativePtr, + new StackTrace()); + while (this.IsBuildInProgress) + Thread.Sleep(100); + } #if VeryVerboseLog - Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); + Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); #endif - this.garbage.Dispose(); + this.Garbage.Dispose(); + return newRefCount; + + case IRefCountable.RefCountResult.AlreadyDisposed: + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + default: + throw new InvalidOperationException(); + } } public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) @@ -201,8 +220,8 @@ internal sealed partial class FontAtlasFactory private readonly object syncRootPostPromotion = new(); private readonly object syncRoot = new(); - private Task buildTask = EmptyTask; - private FontAtlasBuiltData builtData; + private Task buildTask = EmptyTask; + private FontAtlasBuiltData? builtData; private int buildSuppressionCounter; private bool buildSuppressionSuppressed; @@ -275,7 +294,8 @@ internal sealed partial class FontAtlasFactory lock (this.syncRoot) { this.buildTask.ToDisposableIgnoreExceptions().Dispose(); - this.builtData.Dispose(); + this.builtData?.Release(); + this.builtData = null; } } @@ -303,7 +323,7 @@ internal sealed partial class FontAtlasFactory get { lock (this.syncRoot) - return this.builtData.Atlas; + return this.builtData?.Atlas ?? default; } } @@ -311,7 +331,7 @@ internal sealed partial class FontAtlasFactory public Task BuildTask => this.buildTask; /// - public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); + public bool HasBuiltAtlas => !(this.builtData?.Atlas.IsNull() ?? true); /// public bool IsGlobalScaled { get; } @@ -474,13 +494,13 @@ internal sealed partial class FontAtlasFactory var rebuildIndex = ++this.buildIndex; return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); - async Task BuildInner(Task unused) + async Task BuildInner(Task unused) { Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync)); lock (this.syncRoot) { if (this.buildIndex != rebuildIndex) - return default; + return null; } var res = await this.RebuildFontsPrivate(true, scale); @@ -512,8 +532,10 @@ internal sealed partial class FontAtlasFactory return; } - this.builtData.ExplicitDisposeIgnoreExceptions(); + var prevBuiltData = this.builtData; this.builtData = data; + prevBuiltData.ExplicitDisposeIgnoreExceptions(); + this.buildTask = EmptyTask; foreach (var substance in data.Substances) substance.Manager.Substance = substance; @@ -570,6 +592,9 @@ internal sealed partial class FontAtlasFactory } } + foreach (var substance in data.Substances) + substance.Manager.InvokeFontHandleImFontChanged(); + #if VeryVerboseLog Log.Verbose("[{name}] Built from {source}.", this.Name, source); #endif @@ -610,12 +635,14 @@ internal sealed partial class FontAtlasFactory var sw = new Stopwatch(); sw.Start(); - var res = default(FontAtlasBuiltData); + FontAtlasBuiltData? res = null; nint atlasPtr = 0; BuildToolkit? toolkit = null; try { - res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + res = new(this, scale); + foreach (var fhm in this.fontHandleManagers) + res.InitialAddSubstance(fhm.NewSubstance(res)); unsafe { atlasPtr = (nint)res.Atlas.NativePtr; @@ -646,9 +673,11 @@ internal sealed partial class FontAtlasFactory res.IsBuildInProgress = false; toolkit.Dispose(); - res.Dispose(); + res.Release(); - res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + res = new(this, scale); + foreach (var fhm in this.fontHandleManagers) + res.InitialAddSubstance(fhm.NewSubstance(res)); unsafe { atlasPtr = (nint)res.Atlas.NativePtr; @@ -715,8 +744,12 @@ internal sealed partial class FontAtlasFactory nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); - res.IsBuildInProgress = false; - res.Dispose(); + if (res is not null) + { + res.IsBuildInProgress = false; + res.Release(); + } + throw; } finally diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index feda47a8a..c05b3a96d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Disposables; +using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.GameFonts; @@ -53,6 +54,11 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal this.FontStyle = style; } + /// + public event Action? ImFontChanged; + + private event Action? Disposed; + /// /// Provider for for `common/font/fontNN.tex`. /// @@ -113,17 +119,86 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal { this.manager?.FreeFontHandle(this); this.manager = null; + this.Disposed?.InvokeSafely(this); + this.ImFontChanged = null; + } + + /// + public IFontHandle.ImFontLocked Lock() + { + IFontHandleSubstance? prevSubstance = default; + while (true) + { + var substance = this.ManagerNotDisposed.Substance; + if (substance is null) + throw new InvalidOperationException(); + if (substance == prevSubstance) + throw new ObjectDisposedException(nameof(DelegateFontHandle)); + + prevSubstance = substance; + try + { + substance.DataRoot.AddRef(); + } + catch (ObjectDisposedException) + { + continue; + } + + try + { + var fontPtr = substance.GetFontPtr(this); + if (fontPtr.IsNull()) + continue; + return new(fontPtr, substance.DataRoot); + } + finally + { + substance.DataRoot.Release(); + } + } } /// public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + /// + public Task WaitAsync() + { + if (this.Available) + return Task.FromResult(this); + + var tcs = new TaskCompletionSource(); + this.ImFontChanged += OnImFontChanged; + this.Disposed += OnImFontChanged; + if (this.Available) + OnImFontChanged(this); + return tcs.Task; + + void OnImFontChanged(IFontHandle unused) + { + if (tcs.Task.IsCompletedSuccessfully) + return; + + this.ImFontChanged -= OnImFontChanged; + this.Disposed -= OnImFontChanged; + if (this.manager is null) + tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); + else + tcs.SetResult(this); + } + } + + /// + public override string ToString() => $"{nameof(GamePrebakedFontHandle)}({this.FontStyle})"; + /// /// Manager for s. /// internal sealed class HandleManager : IFontHandleManager { private readonly Dictionary gameFontsRc = new(); + private readonly HashSet handles = new(); private readonly object syncRoot = new(); /// @@ -154,8 +229,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public void Dispose() { - this.Substance?.Dispose(); - this.Substance = null; + // empty } /// @@ -165,6 +239,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal bool suggestRebuild; lock (this.syncRoot) { + this.handles.Add(handle); this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; } @@ -183,6 +258,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal lock (this.syncRoot) { + this.handles.Remove(ggfh); if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) return; @@ -192,10 +268,20 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } /// - public IFontHandleSubstance NewSubstance() + public void InvokeFontHandleImFontChanged() + { + if (this.Substance is not HandleSubstance hs) + return; + + foreach (var handle in hs.RelevantHandles) + handle.ImFontChanged?.InvokeSafely(handle); + } + + /// + public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { lock (this.syncRoot) - return new HandleSubstance(this, this.gameFontsRc.Keys); + return new HandleSubstance(this, dataRoot, this.handles.ToArray(), this.gameFontsRc.Keys); } } @@ -218,14 +304,32 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// Initializes a new instance of the class. /// /// The manager. + /// The data root. + /// The relevant handles. /// The game font styles. - public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) + public HandleSubstance( + HandleManager manager, + IRefCountable dataRoot, + GamePrebakedFontHandle[] relevantHandles, + IEnumerable gameFontStyles) { + // We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot. + this.handleManager = manager; - Service.Get(); + this.DataRoot = dataRoot; + this.RelevantHandles = relevantHandles; this.gameFontStyles = new(gameFontStyles); } + /// + /// Gets the relevant handles. + /// + // Not owned by this class. Do not dispose. + public GamePrebakedFontHandle[] RelevantHandles { get; } + + /// + public IRefCountable DataRoot { get; } + /// public IFontHandleManager Manager => this.handleManager; @@ -240,6 +344,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public void Dispose() { + // empty } /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs index 93c688608..7066817b7 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// @@ -27,6 +29,12 @@ internal interface IFontHandleManager : IDisposable /// /// Creates a new substance of the font atlas. /// + /// The data root. /// The new substance. - IFontHandleSubstance NewSubstance(); + IFontHandleSubstance NewSubstance(IRefCountable dataRoot); + + /// + /// Invokes . + /// + void InvokeFontHandleImFontChanged(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs index c800c30ac..73c14efc1 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -9,6 +9,11 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// internal interface IFontHandleSubstance : IDisposable { + /// + /// Gets the data root relevant to this instance of . + /// + IRefCountable DataRoot { get; } + /// /// Gets the manager relevant to this instance of . /// diff --git a/Dalamud/Utility/IRefCountable.cs b/Dalamud/Utility/IRefCountable.cs new file mode 100644 index 000000000..76d1059d1 --- /dev/null +++ b/Dalamud/Utility/IRefCountable.cs @@ -0,0 +1,77 @@ +using System.Diagnostics; +using System.Threading; + +namespace Dalamud.Utility; + +/// +/// Interface for reference counting. +/// +internal interface IRefCountable : IDisposable +{ + /// + /// Result for . + /// + public enum RefCountResult + { + /// + /// The object still has remaining references. No futher action should be done. + /// + StillAlive = 1, + + /// + /// The last reference to the object has been released. The object should be fully released. + /// + FinalRelease = 2, + + /// + /// The object already has been disposed. may be thrown. + /// + AlreadyDisposed = 3, + } + + /// + /// Adds a reference to this reference counted object. + /// + /// The new number of references. + int AddRef(); + + /// + /// Releases a reference from this reference counted object.
+ /// When all references are released, the object will be fully disposed. + ///
+ /// The new number of references. + int Release(); + + /// + /// Alias for . + /// + void IDisposable.Dispose() => this.Release(); + + /// + /// Alters by . + /// + /// The delta to the reference count. + /// The reference to the reference count. + /// The new reference count. + /// The followup action that should be done. + public static RefCountResult AlterRefCount(int delta, ref int refCount, out int newRefCount) + { + Debug.Assert(delta is 1 or -1, "delta must be 1 or -1"); + + while (true) + { + var refCountCopy = refCount; + if (refCountCopy <= 0) + { + newRefCount = refCountCopy; + return RefCountResult.AlreadyDisposed; + } + + newRefCount = refCountCopy + delta; + if (refCountCopy != Interlocked.CompareExchange(ref refCount, newRefCount, refCountCopy)) + continue; + + return newRefCount == 0 ? RefCountResult.FinalRelease : RefCountResult.StillAlive; + } + } +} From 967ae973084e843d8313df7e7458d2cffa678459 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 03:41:26 +0900 Subject: [PATCH 44/71] Expose wrapped default font handle --- .../Interface/Internal/InterfaceManager.cs | 35 ++++-- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 4 + .../Interface/ManagedFontAtlas/IFontHandle.cs | 7 +- Dalamud/Interface/UiBuilder.cs | 106 ++++++++++++++++-- 4 files changed, 131 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 8915b3e3d..62f9145bf 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -13,7 +13,6 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Hooking.WndProcHook; -using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; @@ -87,9 +86,6 @@ internal class InterfaceManager : IDisposable, IServiceType private Hook? resizeBuffersHook; private IFontAtlas? dalamudAtlas; - private IFontHandle.IInternal? defaultFontHandle; - private IFontHandle.IInternal? iconFontHandle; - private IFontHandle.IInternal? monoFontHandle; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -131,19 +127,34 @@ internal class InterfaceManager : IDisposable, IServiceType /// Gets the default ImGui font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr DefaultFont => WhenFontsReady().DefaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets an included FontAwesome icon font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr IconFont => WhenFontsReady().IconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets an included monospaced font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr MonoFont => WhenFontsReady().MonoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + + /// + /// Gets the default font handle. + /// + public IFontHandle.IInternal? DefaultFontHandle { get; private set; } + + /// + /// Gets the icon font handle. + /// + public IFontHandle.IInternal? IconFontHandle { get; private set; } + + /// + /// Gets the mono font handle. + /// + public IFontHandle.IInternal? MonoFontHandle { get; private set; } /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. @@ -691,9 +702,9 @@ internal class InterfaceManager : IDisposable, IServiceType .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); using (this.dalamudAtlas.SuppressAutoRebuild()) { - this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.DefaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); - this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.IconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddFontAwesomeIconFont( new() @@ -702,7 +713,7 @@ internal class InterfaceManager : IDisposable, IServiceType GlyphMinAdvanceX = DefaultFontSizePx, GlyphMaxAdvanceX = DefaultFontSizePx, }))); - this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.MonoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddDalamudAssetFont( DalamudAsset.InconsolataRegular, @@ -715,12 +726,12 @@ internal class InterfaceManager : IDisposable, IServiceType // Use font handles directly. // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); + tk.CopyGlyphsAcrossFonts(this.DefaultFontHandle.ImFont, this.MonoFontHandle.ImFont, true); // Update default font unsafe { - ImGui.GetIO().NativePtr->FontDefault = this.defaultFontHandle.ImFont; + ImGui.GetIO().NativePtr->FontDefault = this.DefaultFontHandle.ImFont; } // Broadcast to auto-rebuilding instances diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index ec3e66e9a..491292f9d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -122,6 +122,10 @@ public interface IFontAtlas : IDisposable /// Note that would not necessarily get changed from calling this function. /// /// If is . + /// + /// Using this method will block the main thread on rebuilding fonts, effectively calling + /// from the main thread. Consider migrating to . + /// void BuildFontsOnNextFrame(); /// diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 81ce84a63..eb57b815f 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -12,7 +12,12 @@ namespace Dalamud.Interface.ManagedFontAtlas; public interface IFontHandle : IDisposable { /// - /// Called when the built instance of has been changed. + /// Called when the built instance of has been changed.
+ /// This event will be invoked on the same thread with + /// ., + /// when the build step is .
+ /// See , , and + /// . ///
event Action ImFontChanged; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 87e3b9032..43912f224 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -41,6 +41,10 @@ public sealed class UiBuilder : IDisposable private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; + private IFontHandle? defaultFontHandle; + private IFontHandle? iconFontHandle; + private IFontHandle? monoFontHandle; + /// /// Initializes a new instance of the class and registers it. /// You do not have to call this manually. @@ -103,7 +107,14 @@ public sealed class UiBuilder : IDisposable /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. /// - [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + /// + /// To add your custom font, use . or + /// .
+ /// To be notified on font changes after fonts are built, use + /// ..
+ /// For all other purposes, use .. + ///
+ [Obsolete("See remarks.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? BuildFonts; @@ -113,6 +124,13 @@ public sealed class UiBuilder : IDisposable /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
+ /// + /// To add your custom font, use . or + /// .
+ /// To be notified on font changes after fonts are built, use + /// ..
+ /// For all other purposes, use .. + ///
[Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? AfterBuildFonts; @@ -143,6 +161,23 @@ public sealed class UiBuilder : IDisposable /// Gets the default Dalamud font - supporting all game languages and icons.
/// Accessing this static property outside of is dangerous and not supported. ///
+ public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; + + /// + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ public static ImFontPtr IconFont => InterfaceManager.IconFont; + + /// + /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ public static ImFontPtr MonoFont => InterfaceManager.MonoFont; + + /// + /// Gets the handle to the default Dalamud font - supporting all game languages and icons. + /// /// /// A font handle corresponding to this font can be obtained with: /// @@ -151,11 +186,15 @@ public sealed class UiBuilder : IDisposable /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); /// /// - public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; + public IFontHandle DefaultFontHandle => + this.defaultFontHandle ??= + this.scopedFinalizer.Add( + new FontHandleWrapper( + this.InterfaceManagerWithScene?.DefaultFontHandle + ?? throw new InvalidOperationException("Scene is not yet ready."))); /// - /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid. ///
/// /// A font handle corresponding to this font can be obtained with: @@ -165,11 +204,15 @@ public sealed class UiBuilder : IDisposable /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); ///
/// - public static ImFontPtr IconFont => InterfaceManager.IconFont; + public IFontHandle IconFontHandle => + this.iconFontHandle ??= + this.scopedFinalizer.Add( + new FontHandleWrapper( + this.InterfaceManagerWithScene?.IconFontHandle + ?? throw new InvalidOperationException("Scene is not yet ready."))); /// - /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular. ///
/// /// A font handle corresponding to this font can be obtained with: @@ -181,7 +224,12 @@ public sealed class UiBuilder : IDisposable /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); ///
/// - public static ImFontPtr MonoFont => InterfaceManager.MonoFont; + public IFontHandle MonoFontHandle => + this.monoFontHandle ??= + this.scopedFinalizer.Add( + new FontHandleWrapper( + this.InterfaceManagerWithScene?.MonoFontHandle + ?? throw new InvalidOperationException("Scene is not yet ready."))); /// /// Gets the game's active Direct3D device. @@ -660,4 +708,46 @@ public sealed class UiBuilder : IDisposable { this.ResizeBuffers?.InvokeSafely(); } + + private class FontHandleWrapper : IFontHandle + { + private IFontHandle? wrapped; + + public FontHandleWrapper(IFontHandle wrapped) + { + this.wrapped = wrapped; + this.wrapped.ImFontChanged += this.WrappedOnImFontChanged; + } + + public event Action? ImFontChanged; + + public Exception? LoadException => + this.wrapped!.LoadException ?? new ObjectDisposedException(nameof(FontHandleWrapper)); + + public bool Available => this.wrapped?.Available ?? false; + + public void Dispose() + { + if (this.wrapped is not { } w) + return; + + this.wrapped = null; + w.ImFontChanged -= this.WrappedOnImFontChanged; + // Note: do not dispose w; we do not own it + } + + public IFontHandle.ImFontLocked Lock() => + this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + + public IFontHandle.FontPopper Push() => + this.wrapped?.Push() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + + public Task WaitAsync() => + this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) ?? + throw new ObjectDisposedException(nameof(FontHandleWrapper)); + + public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped})"; + + private void WrappedOnImFontChanged(IFontHandle obj) => this.ImFontChanged.InvokeSafely(this); + } } From 0701d7805a94723eb8ec8a948c5e5279325e922f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:07:21 +0900 Subject: [PATCH 45/71] BuildFonts remarks --- Dalamud/Interface/UiBuilder.cs | 45 +++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 43912f224..02decf103 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -13,6 +13,7 @@ using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -103,7 +104,7 @@ public sealed class UiBuilder : IDisposable /// /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
- /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt + /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
@@ -112,7 +113,36 @@ public sealed class UiBuilder : IDisposable /// .
/// To be notified on font changes after fonts are built, use /// ..
- /// For all other purposes, use .. + /// For all other purposes, use ..
+ ///
+ /// Note that you will be calling above functions once, instead of every time inside a build step change callback. + /// For example, you can make all font handles from your plugin constructor, and then use the created handles during + /// event, by using in a scope.
+ /// You may dispose your font handle anytime, as long as it's not in use in . + /// Font handles may be constructed anytime, as long as the owner or + /// is not disposed.
+ ///
+ /// If you were storing , consider if the job can be achieved solely by using + /// without directly using an instance of .
+ /// If you do need it, evaluate if you need to access fonts outside the main thread.
+ /// If it is the case, use to obtain a safe-to-access instance of + /// , once resolves.
+ /// Otherwise, use , and obtain the instance of via + /// . Do not let the escape the using scope.
+ ///
+ /// If your plugin sets to a non-default value, then + /// should be accessed using + /// , as the font handle member variables are only available + /// once drawing facilities are available.
+ ///
+ /// Examples:
+ /// * .
+ /// * .
+ /// * ctor.
+ /// * : + /// note how a new instance of is constructed, and + /// is called from another function, without having to manually + /// initialize font rebuild process. /// [Obsolete("See remarks.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] @@ -120,18 +150,11 @@ public sealed class UiBuilder : IDisposable /// /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
- /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt + /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
- /// - /// To add your custom font, use . or - /// .
- /// To be notified on font changes after fonts are built, use - /// ..
- /// For all other purposes, use .. - ///
- [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + [Obsolete($"See remarks for {nameof(BuildFonts)}.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? AfterBuildFonts; From 127b91f4b0d05ca08591f3b8cfbda8a6d9f706ea Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:12:40 +0900 Subject: [PATCH 46/71] Fix doc --- Dalamud/Interface/UiBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 02decf103..c27c9ab84 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -140,9 +140,9 @@ public sealed class UiBuilder : IDisposable /// * .
/// * ctor.
/// * : - /// note how a new instance of is constructed, and - /// is called from another function, without having to manually - /// initialize font rebuild process. + /// note how the construction of a new instance of and + /// call of are done in different functions, + /// without having to manually initiate font rebuild process. /// [Obsolete("See remarks.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] From af1133f99973af30d01b1946f7513810320a6243 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:21:26 +0900 Subject: [PATCH 47/71] Determine optional assets availability on startup --- Dalamud.CorePlugin/PluginImpl.cs | 4 ++++ Dalamud/Storage/Assets/DalamudAssetManager.cs | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index ef99f6def..96d212dd3 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -69,6 +69,10 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi; + this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += fc => + { + Log.Information($"CorePlugin : DefaultFontHandle.ImFontChanged called {fc}"); + }; Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = "Access the plugin." }); diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 70a91c4bf..7edb1c61d 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -69,6 +69,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA .Select(x => x.ToContentDisposedTask())) .ContinueWith(_ => loadTimings.Dispose()), "Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available."); + + Task.WhenAll( + Enum.GetValues() + .Where(x => x is not DalamudAsset.Empty4X4) + .Where(x => x.GetAttribute()?.Required is false) + .Select(this.CreateStreamAsync) + .Select(x => x.ToContentDisposedTask())) + .ContinueWith(r => Log.Verbose($"Optional assets load state: {r}")); } /// From 3e3297f7a8eeb3edcd83021ac90c25fb1d3f0482 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:49:51 +0900 Subject: [PATCH 48/71] Use Lock instead of .ImFont --- Dalamud/Interface/Internal/InterfaceManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 62f9145bf..159ae15bf 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -725,13 +725,16 @@ internal class InterfaceManager : IDisposable, IServiceType // Do not use DefaultFont, IconFont, and MonoFont. // Use font handles directly. + using var defaultFont = this.DefaultFontHandle.Lock(); + using var monoFont = this.MonoFontHandle.Lock(); + // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(this.DefaultFontHandle.ImFont, this.MonoFontHandle.ImFont, true); + tk.CopyGlyphsAcrossFonts(defaultFont, monoFont, true); // Update default font unsafe { - ImGui.GetIO().NativePtr->FontDefault = this.DefaultFontHandle.ImFont; + ImGui.GetIO().NativePtr->FontDefault = defaultFont; } // Broadcast to auto-rebuilding instances From a409ea60d6442a37c218dee954e27fc4e5e113d9 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:54:35 +0900 Subject: [PATCH 49/71] Update docs --- .../IFontAtlasBuildToolkitPreBuild.cs | 14 ++++++++------ Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index cb8a27a54..38d8d2fe8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -54,11 +54,11 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds a font from memory region allocated using .
- /// It WILL crash if you try to use a memory pointer allocated in some other way.
- /// + /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// /// Do NOT call on the once this function has /// been called, unless is set and the function has thrown an error. - ///
+ /// ///
/// Memory address for the data allocated using . /// The size of the font file.. @@ -81,9 +81,11 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds a font from memory region allocated using .
- /// It WILL crash if you try to use a memory pointer allocated in some other way.
- /// Do NOT call on the once this - /// function has been called. + /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// + /// Do NOT call on the once this function has + /// been called, unless is set and the function has thrown an error. + /// ///
/// Memory address for the data allocated using . /// The size of the font file.. diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index eb57b815f..877cd60c9 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -53,7 +53,8 @@ public interface IFontHandle : IDisposable /// /// Locks the fully constructed instance of corresponding to the this - /// , for read-only use in any thread. + /// , for use in any thread.
+ /// Modification of the font will exhibit undefined behavior if some other thread also uses the font. ///
/// An instance of that must be disposed after use. /// From 500df36cae48bbf0132cec86248dcc4605b26fd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Jan 2024 23:48:44 +0000 Subject: [PATCH 50/71] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index bbc4b9942..e9341bb30 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit bbc4b994254d6913f51da3a20fad9bf4b8c986e5 +Subproject commit e9341bb3038bf4200300f21be4a8629525d15596 From 29b3e0aa97683d1dcb11421fd3aef0b909b05119 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 13:15:36 +0900 Subject: [PATCH 51/71] Make IFontHandle.Push return IDisposable, and add IFontHandle.Pop --- Dalamud/Dalamud.csproj | 1 + Dalamud/Interface/GameFonts/GameFontHandle.cs | 4 +- .../Interface/Internal/InterfaceManager.cs | 7 ++ .../Widgets/GamePrebakedFontsTestWidget.cs | 20 ++++- .../Interface/ManagedFontAtlas/IFontHandle.cs | 49 +++--------- .../Internals/DelegateFontHandle.cs | 36 ++++++++- .../Internals/GamePrebakedFontHandle.cs | 33 +++++++- .../Internals/SimplePushedFont.cs | 78 +++++++++++++++++++ Dalamud/Interface/UiBuilder.cs | 4 +- 9 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index ba044a555..f58a0c47a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -70,6 +70,7 @@ + all diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 6591ce0fe..7bda27eae 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -72,8 +72,8 @@ public sealed class GameFontHandle : IFontHandle /// An that can be used to pop the font on dispose. public IDisposable Push() => this.fontHandle.Push(); - /// - IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push(); + /// + public void Pop() => this.fontHandle.Pop(); /// public Task WaitAsync() => this.fontHandle.WaitAsync(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 159ae15bf..e1b714ee8 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -230,6 +230,11 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; + /// + /// Gets the number of calls to so far. + /// + public long CumulativePresentCalls { get; private set; } + /// /// Dispose of managed and unmanaged resources. /// @@ -647,6 +652,8 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { + this.CumulativePresentCalls++; + Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index b3b57343c..7b649a895 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -163,6 +163,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable .ToArray()); var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); + var counter = 0; foreach (var (family, items) in this.fontHandles) { if (!ImGui.CollapsingHeader($"{family} Family")) @@ -188,10 +189,21 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable { if (!this.useGlobalScale) ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); - using var pushPop = handle.Value.Push(); - ImGuiNative.igTextUnformatted( - this.testStringBuffer.Data, - this.testStringBuffer.Data + this.testStringBuffer.Length); + if (counter++ % 2 == 0) + { + using var pushPop = handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + } + else + { + handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + handle.Value.Pop(); + } } } finally diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 877cd60c9..94edc9777 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -65,17 +65,23 @@ public interface IFontHandle : IDisposable ImFontLocked Lock(); /// - /// Pushes the current font into ImGui font stack using , if available.
+ /// Pushes the current font into ImGui font stack, if available.
/// Use to access the current font.
/// You may not access the font once you dispose this object. ///
- /// A disposable object that will call (1) on dispose. + /// A disposable object that will pop the font on dispose. /// If called outside of the main thread. /// - /// Only intended for use with using keywords, such as using (handle.Push()).
- /// Should you store or transfer the return value to somewhere else, use as the type. + /// This function uses , and may do extra things. + /// Use or to undo this operation. + /// Do not use . ///
- FontPopper Push(); + IDisposable Push(); + + /// + /// Pops the font pushed to ImGui using , cleaning up any extra information as needed. + /// + void Pop(); /// /// Waits for to become true. @@ -124,37 +130,4 @@ public interface IFontHandle : IDisposable this.ImFont = default; } } - - /// - /// The wrapper for popping fonts. - /// - public struct FontPopper : IDisposable - { - private int count; - - /// - /// Initializes a new instance of the struct. - /// - /// The font to push. - /// Whether to push. - internal FontPopper(ImFontPtr fontPtr, bool push) - { - if (!push) - return; - - ThreadSafety.AssertMainThread(); - - this.count = 1; - ImGui.PushFont(fontPtr); - } - - /// - public void Dispose() - { - ThreadSafety.AssertMainThread(); - - while (this.count-- > 0) - ImGui.PopFont(); - } - } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index f50967fae..e1c18e923 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -2,12 +2,15 @@ using System.Linq; using System.Threading.Tasks; +using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; using Dalamud.Utility; using ImGuiNET; +using Serilog; + namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// @@ -15,7 +18,10 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// internal class DelegateFontHandle : IFontHandle.IInternal { + private readonly List pushedFonts = new(8); + private IFontHandleManager? manager; + private long lastCumulativePresentCalls; /// /// Initializes a new instance of the class. @@ -53,6 +59,8 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public void Dispose() { + if (this.pushedFonts.Count > 0) + Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); this.manager?.FreeFontHandle(this); this.manager = null; this.Disposed?.InvokeSafely(this); @@ -96,7 +104,33 @@ internal class DelegateFontHandle : IFontHandle.IInternal } /// - public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + public IDisposable Push() + { + ThreadSafety.AssertMainThread(); + var cumulativePresentCalls = Service.GetNullable()?.CumulativePresentCalls ?? 0L; + if (this.lastCumulativePresentCalls != cumulativePresentCalls) + { + this.lastCumulativePresentCalls = cumulativePresentCalls; + if (this.pushedFonts.Count > 0) + { + Log.Warning( + $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + + $"You might be missing a call to {nameof(this.Pop)}."); + this.pushedFonts.Clear(); + } + } + + var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available); + this.pushedFonts.Add(rented); + return rented; + } + + /// + public void Pop() + { + ThreadSafety.AssertMainThread(); + this.pushedFonts[^1].Dispose(); + } /// public Task WaitAsync() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index c05b3a96d..0e8301785 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -16,6 +16,8 @@ using ImGuiNET; using Lumina.Data.Files; +using Serilog; + using Vector4 = System.Numerics.Vector4; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -35,7 +37,10 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); + private readonly List pushedFonts = new(8); + private IFontHandleManager? manager; + private long lastCumulativePresentCalls; /// /// Initializes a new instance of the class. @@ -160,7 +165,33 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } /// - public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + public IDisposable Push() + { + ThreadSafety.AssertMainThread(); + var cumulativePresentCalls = Service.GetNullable()?.CumulativePresentCalls ?? 0L; + if (this.lastCumulativePresentCalls != cumulativePresentCalls) + { + this.lastCumulativePresentCalls = cumulativePresentCalls; + if (this.pushedFonts.Count > 0) + { + Log.Warning( + $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + + $"You might be missing a call to {nameof(this.Pop)}."); + this.pushedFonts.Clear(); + } + } + + var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available); + this.pushedFonts.Add(rented); + return rented; + } + + /// + public void Pop() + { + ThreadSafety.AssertMainThread(); + this.pushedFonts[^1].Dispose(); + } /// public Task WaitAsync() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs new file mode 100644 index 000000000..3f7255386 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Diagnostics; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +using Microsoft.Extensions.ObjectPool; + +using Serilog; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Reusable font push/popper. +/// +internal sealed class SimplePushedFont : IDisposable +{ + // Using constructor instead of DefaultObjectPoolProvider, since we do not want the pool to call Dispose. + private static readonly ObjectPool Pool = + new DefaultObjectPool(new DefaultPooledObjectPolicy()); + + private List? stack; + private ImFontPtr font; + + /// + /// Pushes the font, and return an instance of . + /// + /// The -private stack. + /// The font pointer being pushed. + /// Whether to push. + /// this. + public static SimplePushedFont Rent(List stack, ImFontPtr fontPtr, bool push) + { + push &= !fontPtr.IsNull(); + + var rented = Pool.Get(); + Debug.Assert(rented.font.IsNull(), "Rented object must not have its font set"); + rented.stack = stack; + + if (push) + { + rented.font = fontPtr; + ImGui.PushFont(fontPtr); + } + + return rented; + } + + /// + public unsafe void Dispose() + { + if (this.stack is null || !ReferenceEquals(this.stack[^1], this)) + { + throw new InvalidOperationException("Tried to pop a non-pushed font."); + } + + this.stack.RemoveAt(this.stack.Count - 1); + + if (!this.font.IsNull()) + { + if (ImGui.GetFont().NativePtr == this.font.NativePtr) + { + ImGui.PopFont(); + } + else + { + Log.Warning( + $"{nameof(IFontHandle.Pop)}: The font currently being popped does not match the pushed font. " + + $"Doing nothing."); + } + } + + this.font = default; + this.stack = null; + Pool.Return(this); + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index c27c9ab84..1134704ee 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -762,9 +762,11 @@ public sealed class UiBuilder : IDisposable public IFontHandle.ImFontLocked Lock() => this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); - public IFontHandle.FontPopper Push() => + public IDisposable Push() => this.wrapped?.Push() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + public void Pop() => this.wrapped?.Pop(); + public Task WaitAsync() => this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); From fc4d08927b82f332b81f318091226b06cd0a993c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 15:11:31 +0900 Subject: [PATCH 52/71] Fix Dalamud Configuration revert not rebuilding fonts --- Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 027e1a571..c325028e1 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -69,6 +69,7 @@ internal class SettingsWindow : Window var fontAtlasFactory = Service.Get(); var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; + rebuildFont |= !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale); ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; fontAtlasFactory.UseAxisOverride = null; From d1291364e01ba7e031f611397eae84c1d32b64d5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 19:30:09 +0900 Subject: [PATCH 53/71] Fix FontHandleWrapper and some docs --- Dalamud/Interface/UiBuilder.cs | 22 +++++++++---------- .../Storage/Assets/IDalamudAssetManager.cs | 6 +++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 1134704ee..d01d307c3 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -744,10 +744,12 @@ public sealed class UiBuilder : IDisposable public event Action? ImFontChanged; - public Exception? LoadException => - this.wrapped!.LoadException ?? new ObjectDisposedException(nameof(FontHandleWrapper)); + public Exception? LoadException => this.WrappedNotDisposed.LoadException; - public bool Available => this.wrapped?.Available ?? false; + public bool Available => this.WrappedNotDisposed.Available; + + private IFontHandle WrappedNotDisposed => + this.wrapped ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); public void Dispose() { @@ -759,19 +761,17 @@ public sealed class UiBuilder : IDisposable // Note: do not dispose w; we do not own it } - public IFontHandle.ImFontLocked Lock() => - this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + public IFontHandle.ImFontLocked Lock() => this.WrappedNotDisposed.Lock(); - public IDisposable Push() => - this.wrapped?.Push() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + public IDisposable Push() => this.WrappedNotDisposed.Push(); - public void Pop() => this.wrapped?.Pop(); + public void Pop() => this.WrappedNotDisposed.Pop(); public Task WaitAsync() => - this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) ?? - throw new ObjectDisposedException(nameof(FontHandleWrapper)); + this.WrappedNotDisposed.WaitAsync().ContinueWith(_ => (IFontHandle)this); - public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped})"; + public override string ToString() => + $"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})"; private void WrappedOnImFontChanged(IFontHandle obj) => this.ImFontChanged.InvokeSafely(this); } diff --git a/Dalamud/Storage/Assets/IDalamudAssetManager.cs b/Dalamud/Storage/Assets/IDalamudAssetManager.cs index 4fb83df80..1202891b8 100644 --- a/Dalamud/Storage/Assets/IDalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/IDalamudAssetManager.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; using System.IO; using System.Threading.Tasks; @@ -64,8 +65,9 @@ internal interface IDalamudAssetManager /// /// The texture asset. /// The default return value, if the asset is not ready for whatever reason. - /// The texture wrap. + /// The texture wrap. Can be null only if is null. [Pure] + [return: NotNullIfNotNull(nameof(defaultWrap))] IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap); /// From 5479149e79a9a91aff74ebbb1c4adf250ca93137 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 20:51:29 +0900 Subject: [PATCH 54/71] Lock font resources on Push and miscellaneous direct accesses These changes ensure that using a font under some other thread's ownership from the UI thread for rendering into ImGui purposes always work. * `FontHandle`: * Moved common code from `DelegateFontHandle` and `GamePrebakedFontHandle`. * Added `LockUntilPostFrame` so that the obtained `ImFontPtr` and its accompanying resources are kept valid until everything is rendered. * Added more code comments to `Try/Lock`. * Moved font access thread checking logic from `InterfaceManager` to `LockUntilPostFrame`. * `Push`ing a font will now also perform `LockUntilPostFrame`. * `GameFontHandle`: Make the property `ImFont` a forwarder to `FontHandle.LockUntilPostFrame`. * `InterfaceManager`: * Added companion logic to `FontHandle.LockUntilPostFrame`. * Accessing default/icon/mono fonts will forward to `FontHandle.LockUntilPostFrame`. * Changed `List` to `ConcurrentBag` as texture disposal can be done outside the main thread, and a race condition is possible. --- Dalamud/Interface/GameFonts/GameFontHandle.cs | 26 +- .../Interface/Internal/InterfaceManager.cs | 92 +++--- .../Interface/ManagedFontAtlas/IFontHandle.cs | 21 +- .../Internals/DelegateFontHandle.cs | 135 +-------- .../ManagedFontAtlas/Internals/FontHandle.cs | 263 ++++++++++++++++++ .../Internals/GamePrebakedFontHandle.cs | 132 +-------- .../Internals/SimplePushedFont.cs | 7 +- Dalamud/Interface/UiBuilder.cs | 2 +- 8 files changed, 331 insertions(+), 347 deletions(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 7bda27eae..4c472c032 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -15,15 +15,16 @@ namespace Dalamud.Interface.GameFonts; [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public sealed class GameFontHandle : IFontHandle { - private readonly IFontHandle.IInternal fontHandle; + private readonly GamePrebakedFontHandle fontHandle; private readonly FontAtlasFactory fontAtlasFactory; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class.
+ /// Ownership of is transferred. ///
- /// The wrapped . + /// The wrapped . /// An instance of . - internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory) + internal GameFontHandle(GamePrebakedFontHandle fontHandle, FontAtlasFactory fontAtlasFactory) { this.fontHandle = fontHandle; this.fontAtlasFactory = fontAtlasFactory; @@ -42,9 +43,15 @@ public sealed class GameFontHandle : IFontHandle /// public bool Available => this.fontHandle.Available; - /// - [Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)] - public ImFontPtr ImFont => this.fontHandle.ImFont; + /// + /// Gets the font.
+ /// Use of this properly is safe only from the UI thread.
+ /// Use if the intended purpose of this property is .
+ /// Futures changes may make simple not enough.
+ /// If you need to access a font outside the UI thread, use . + ///
+ [Obsolete($"Use {nameof(Push)}-{nameof(ImGui.GetFont)} or {nameof(Lock)} instead.", false)] + public ImFontPtr ImFont => this.fontHandle.LockUntilPostFrame(); /// /// Gets the font style. Only applicable for . @@ -66,10 +73,7 @@ public sealed class GameFontHandle : IFontHandle /// public IFontHandle.ImFontLocked Lock() => this.fontHandle.Lock(); - /// - /// Pushes the font. - /// - /// An that can be used to pop the font on dispose. + /// public IDisposable Push() => this.fontHandle.Push(); /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index e1b714ee8..25baa5e29 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -20,8 +21,6 @@ using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Plugin.Internal; -using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -63,15 +62,9 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private const int NonMainThreadFontAccessWarningCheckInterval = 10000; - private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); - private static long nextNonMainThreadFontAccessWarningCheck; + private readonly ConcurrentBag deferredDisposeTextures = new(); + private readonly ConcurrentBag deferredDisposeImFontLockeds = new(); - private readonly List deferredDisposeTextures = new(); - - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); @@ -127,34 +120,37 @@ internal class InterfaceManager : IDisposable, IServiceType /// Gets the default ImGui font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr DefaultFont => WhenFontsReady().DefaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr DefaultFont => + WhenFontsReady().DefaultFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault); /// /// Gets an included FontAwesome icon font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr IconFont => WhenFontsReady().IconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr IconFont => + WhenFontsReady().IconFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault); /// /// Gets an included monospaced font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr MonoFont => WhenFontsReady().MonoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr MonoFont => + WhenFontsReady().MonoFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault); /// /// Gets the default font handle. /// - public IFontHandle.IInternal? DefaultFontHandle { get; private set; } + public FontHandle? DefaultFontHandle { get; private set; } /// /// Gets the icon font handle. /// - public IFontHandle.IInternal? IconFontHandle { get; private set; } + public FontHandle? IconFontHandle { get; private set; } /// /// Gets the mono font handle. /// - public IFontHandle.IInternal? MonoFontHandle { get; private set; } + public FontHandle? MonoFontHandle { get; private set; } /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. @@ -408,6 +404,15 @@ internal class InterfaceManager : IDisposable, IServiceType this.deferredDisposeTextures.Add(wrap); } + /// + /// Enqueue an to be disposed at the end of the frame. + /// + /// The disposable. + public void EnqueueDeferredDispose(in IFontHandle.ImFontLocked locked) + { + this.deferredDisposeImFontLockeds.Add(locked); + } + /// /// Get video memory information. /// @@ -466,29 +471,6 @@ internal class InterfaceManager : IDisposable, IServiceType if (im?.dalamudAtlas is not { } atlas) throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call."); - if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) - { - nextNonMainThreadFontAccessWarningCheck = - Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; - var stack = new StackTrace(); - if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) - { - if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) - { - NonMainThreadFontAccessWarning.Add(plugin, new()); - Log.Warning( - "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", - plugin.Name, - stack); - } - } - else - { - // Dalamud internal should be made safe right now - throw new InvalidOperationException("Attempted to access fonts outside the main thread."); - } - } - if (!atlas.HasBuiltAtlas) atlas.BuildTask.GetAwaiter().GetResult(); return im; @@ -673,28 +655,38 @@ internal class InterfaceManager : IDisposable, IServiceType var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); RenderImGui(this.scene!); - this.DisposeTextures(); + this.CleanupPostImGuiRender(); return pRes; } RenderImGui(this.scene!); - this.DisposeTextures(); + this.CleanupPostImGuiRender(); return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } - private void DisposeTextures() + private void CleanupPostImGuiRender() { - if (this.deferredDisposeTextures.Count > 0) + if (!this.deferredDisposeTextures.IsEmpty) { - Log.Verbose("[IM] Disposing {Count} textures", this.deferredDisposeTextures.Count); - foreach (var texture in this.deferredDisposeTextures) + var count = 0; + while (this.deferredDisposeTextures.TryTake(out var d)) { - texture.RealDispose(); + count++; + d.RealDispose(); } - this.deferredDisposeTextures.Clear(); + Log.Verbose("[IM] Disposing {Count} textures", count); + } + + if (!this.deferredDisposeImFontLockeds.IsEmpty) + { + // Not logging; the main purpose of this is to keep resources used for rendering the frame to be kept + // referenced until the resources are actually done being used, and it is expected that this will be + // frequent. + while (this.deferredDisposeImFontLockeds.TryTake(out var d)) + d.Dispose(); } } @@ -709,9 +701,9 @@ internal class InterfaceManager : IDisposable, IServiceType .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); using (this.dalamudAtlas.SuppressAutoRebuild()) { - this.DefaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.DefaultFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); - this.IconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.IconFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddFontAwesomeIconFont( new() @@ -720,7 +712,7 @@ internal class InterfaceManager : IDisposable, IServiceType GlyphMinAdvanceX = DefaultFontSizePx, GlyphMaxAdvanceX = DefaultFontSizePx, }))); - this.MonoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddDalamudAssetFont( DalamudAsset.InconsolataRegular, diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 94edc9777..97d345925 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -21,21 +21,6 @@ public interface IFontHandle : IDisposable /// event Action ImFontChanged; - /// - /// Represents a reference counting handle for fonts. Dalamud internal use only. - /// - internal interface IInternal : IFontHandle - { - /// - /// Gets the font.
- /// Use of this properly is safe only from the UI thread.
- /// Use if the intended purpose of this property is .
- /// Futures changes may make simple not enough.
- /// If you need to access a font outside the UI thread, consider using . - ///
- ImFontPtr ImFont { get; } - } - /// /// Gets the load exception, if it failed to load. Otherwise, it is null. /// @@ -45,7 +30,6 @@ public interface IFontHandle : IDisposable /// Gets a value indicating whether this font is ready for use. ///
/// - /// Once set to true, it will remain true.
/// Use directly if you want to keep the current ImGui font if the font is not ready.
/// Alternatively, use to wait for this property to become true. ///
@@ -103,14 +87,13 @@ public interface IFontHandle : IDisposable private IRefCountable? owner; /// - /// Initializes a new instance of the struct, - /// and incrase the reference count of . + /// Initializes a new instance of the struct. + /// Ownership of reference of is transferred. /// /// The contained font. /// The owner. internal ImFontLocked(ImFontPtr imFont, IRefCountable owner) { - owner.AddRef(); this.ImFont = imFont; this.owner = owner; } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index e1c18e923..53a836511 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -1,164 +1,35 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; using Dalamud.Utility; using ImGuiNET; -using Serilog; - namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// /// A font handle representing a user-callback generated font. /// -internal class DelegateFontHandle : IFontHandle.IInternal +internal sealed class DelegateFontHandle : FontHandle { - private readonly List pushedFonts = new(8); - - private IFontHandleManager? manager; - private long lastCumulativePresentCalls; - /// /// Initializes a new instance of the class. /// /// An instance of . /// Callback for . public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange) + : base(manager) { - this.manager = manager; this.CallOnBuildStepChange = callOnBuildStepChange; } - /// - public event Action? ImFontChanged; - - private event Action? Disposed; - /// /// Gets the function to be called on build step changes. /// public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; } - /// - public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); - - /// - public bool Available => this.ImFont.IsNotNullAndLoaded(); - - /// - public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; - - private IFontHandleManager ManagerNotDisposed => - this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); - - /// - public void Dispose() - { - if (this.pushedFonts.Count > 0) - Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); - this.manager?.FreeFontHandle(this); - this.manager = null; - this.Disposed?.InvokeSafely(this); - this.ImFontChanged = null; - } - - /// - public IFontHandle.ImFontLocked Lock() - { - IFontHandleSubstance? prevSubstance = default; - while (true) - { - var substance = this.ManagerNotDisposed.Substance; - if (substance is null) - throw new InvalidOperationException(); - if (substance == prevSubstance) - throw new ObjectDisposedException(nameof(DelegateFontHandle)); - - prevSubstance = substance; - try - { - substance.DataRoot.AddRef(); - } - catch (ObjectDisposedException) - { - continue; - } - - try - { - var fontPtr = substance.GetFontPtr(this); - if (fontPtr.IsNull()) - continue; - return new(fontPtr, substance.DataRoot); - } - finally - { - substance.DataRoot.Release(); - } - } - } - - /// - public IDisposable Push() - { - ThreadSafety.AssertMainThread(); - var cumulativePresentCalls = Service.GetNullable()?.CumulativePresentCalls ?? 0L; - if (this.lastCumulativePresentCalls != cumulativePresentCalls) - { - this.lastCumulativePresentCalls = cumulativePresentCalls; - if (this.pushedFonts.Count > 0) - { - Log.Warning( - $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + - $"You might be missing a call to {nameof(this.Pop)}."); - this.pushedFonts.Clear(); - } - } - - var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available); - this.pushedFonts.Add(rented); - return rented; - } - - /// - public void Pop() - { - ThreadSafety.AssertMainThread(); - this.pushedFonts[^1].Dispose(); - } - - /// - public Task WaitAsync() - { - if (this.Available) - return Task.FromResult(this); - - var tcs = new TaskCompletionSource(); - this.ImFontChanged += OnImFontChanged; - this.Disposed += OnImFontChanged; - if (this.Available) - OnImFontChanged(this); - return tcs.Task; - - void OnImFontChanged(IFontHandle unused) - { - if (tcs.Task.IsCompletedSuccessfully) - return; - - this.ImFontChanged -= OnImFontChanged; - this.Disposed -= OnImFontChanged; - if (this.manager is null) - tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); - else - tcs.SetResult(this); - } - } - /// /// Manager for s. /// @@ -216,7 +87,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal return; foreach (var handle in hs.RelevantHandles) - handle.ImFontChanged?.InvokeSafely(handle); + handle.InvokeImFontChanged(); } /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs new file mode 100644 index 000000000..93b17f86e --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -0,0 +1,263 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +using ImGuiNET; + +using Serilog; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Default implementation for . +/// +internal abstract class FontHandle : IFontHandle +{ + private const int NonMainThreadFontAccessWarningCheckInterval = 10000; + private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); + private static long nextNonMainThreadFontAccessWarningCheck; + + private readonly InterfaceManager interfaceManager; + private readonly List pushedFonts = new(8); + + private IFontHandleManager? manager; + private long lastCumulativePresentCalls; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + protected FontHandle(IFontHandleManager manager) + { + this.interfaceManager = Service.Get(); + this.manager = manager; + } + + /// + public event Action? ImFontChanged; + + /// + /// Event to be called on the first call. + /// + protected event Action? Disposed; + + /// + public Exception? LoadException => this.Manager.Substance?.GetBuildException(this); + + /// + public bool Available => (this.Manager.Substance?.GetFontPtr(this) ?? default).IsNotNullAndLoaded(); + + /// + /// Gets the associated . + /// + /// When the object has already been disposed. + protected IFontHandleManager Manager => this.manager ?? throw new ObjectDisposedException(this.GetType().Name); + + /// + public void Dispose() + { + if (this.manager is null) + return; + + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Obtains an instance of corresponding to this font handle, + /// to be released after rendering the current frame. + /// + /// The font pointer, or default if unavailble. + /// + /// Behavior is undefined on access outside the main thread. + /// + public ImFontPtr LockUntilPostFrame() + { + if (this.TryLock(out _) is not { } locked) + return default; + + if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) + { + nextNonMainThreadFontAccessWarningCheck = + Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; + var stack = new StackTrace(); + if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) + { + if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) + { + NonMainThreadFontAccessWarning.Add(plugin, new()); + Log.Warning( + "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", + plugin.Name, + stack); + } + } + else + { + // Dalamud internal should be made safe right now + throw new InvalidOperationException("Attempted to access fonts outside the main thread."); + } + } + + this.interfaceManager.EnqueueDeferredDispose(locked); + return locked.ImFont; + } + + /// + /// Attempts to lock the fully constructed instance of corresponding to the this + /// , for use in any thread.
+ /// Modification of the font will exhibit undefined behavior if some other thread also uses the font. + ///
+ /// The error message, if any. + /// + /// An instance of that must be disposed after use on success; + /// null with populated on failure. + /// + /// Still may be thrown. + public IFontHandle.ImFontLocked? TryLock(out string? errorMessage) + { + IFontHandleSubstance? prevSubstance = default; + while (true) + { + var substance = this.Manager.Substance; + + // Does the associated IFontAtlas have a built substance? + if (substance is null) + { + errorMessage = "The font atlas has not been built yet."; + return null; + } + + // Did we loop (because it did not have the requested font), + // and are the fetched substance same between loops? + if (substance == prevSubstance) + { + errorMessage = "The font atlas did not built the requested handle yet."; + return null; + } + + prevSubstance = substance; + + // Try to lock the substance. + try + { + substance.DataRoot.AddRef(); + } + catch (ObjectDisposedException) + { + // If it got invalidated, it's probably because a new substance is incoming. Try again. + continue; + } + + var fontPtr = substance.GetFontPtr(this); + if (fontPtr.IsNull()) + { + // The font for the requested handle is unavailable. Release the reference and try again. + substance.DataRoot.Release(); + continue; + } + + // Transfer the ownership of reference. + errorMessage = null; + return new(fontPtr, substance.DataRoot); + } + } + + /// + public IFontHandle.ImFontLocked Lock() => + this.TryLock(out var errorMessage) ?? throw new InvalidOperationException(errorMessage); + + /// + public IDisposable Push() + { + ThreadSafety.AssertMainThread(); + + // Warn if the client is not properly managing the pushed font stack. + var cumulativePresentCalls = this.interfaceManager.CumulativePresentCalls; + if (this.lastCumulativePresentCalls != cumulativePresentCalls) + { + this.lastCumulativePresentCalls = cumulativePresentCalls; + if (this.pushedFonts.Count > 0) + { + Log.Warning( + $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + + $"You might be missing a call to {nameof(this.Pop)}."); + this.pushedFonts.Clear(); + } + } + + var font = default(ImFontPtr); + if (this.TryLock(out _) is { } locked) + { + font = locked.ImFont; + this.interfaceManager.EnqueueDeferredDispose(locked); + } + + var rented = SimplePushedFont.Rent(this.pushedFonts, font); + this.pushedFonts.Add(rented); + return rented; + } + + /// + public void Pop() + { + ThreadSafety.AssertMainThread(); + this.pushedFonts[^1].Dispose(); + } + + /// + public Task WaitAsync() + { + if (this.Available) + return Task.FromResult(this); + + var tcs = new TaskCompletionSource(); + this.ImFontChanged += OnImFontChanged; + this.Disposed += OnImFontChanged; + if (this.Available) + OnImFontChanged(this); + return tcs.Task; + + void OnImFontChanged(IFontHandle unused) + { + if (tcs.Task.IsCompletedSuccessfully) + return; + + this.ImFontChanged -= OnImFontChanged; + this.Disposed -= OnImFontChanged; + if (this.manager is null) + tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); + else + tcs.SetResult(this); + } + } + + /// + /// Invokes . + /// + protected void InvokeImFontChanged() => this.ImFontChanged.InvokeSafely(this); + + /// + /// Overrideable implementation for . + /// + /// If true, then the function is being called from . + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (this.pushedFonts.Count > 0) + Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); + this.Manager.FreeFontHandle(this); + this.manager = null; + this.Disposed?.InvokeSafely(this); + this.ImFontChanged = null; + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 0e8301785..e062405b8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Disposables; -using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.GameFonts; @@ -16,8 +15,6 @@ using ImGuiNET; using Lumina.Data.Files; -using Serilog; - using Vector4 = System.Numerics.Vector4; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -25,7 +22,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// /// A font handle that uses the game's built-in fonts, optionally with some styling. /// -internal class GamePrebakedFontHandle : IFontHandle.IInternal +internal class GamePrebakedFontHandle : FontHandle { /// /// The smallest value of . @@ -37,17 +34,13 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); - private readonly List pushedFonts = new(8); - - private IFontHandleManager? manager; - private long lastCumulativePresentCalls; - /// /// Initializes a new instance of the class. /// /// An instance of . /// Font to use. public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style) + : base(manager) { if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined) throw new ArgumentOutOfRangeException(nameof(style), style, null); @@ -55,15 +48,9 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal if (style.SizePt <= 0) throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style)); - this.manager = manager; this.FontStyle = style; } - /// - public event Action? ImFontChanged; - - private event Action? Disposed; - /// /// Provider for for `common/font/fontNN.tex`. /// @@ -107,119 +94,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal ///
public GameFontStyle FontStyle { get; } - /// - public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); - - /// - public bool Available => this.ImFont.IsNotNullAndLoaded(); - - /// - public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; - - private IFontHandleManager ManagerNotDisposed => - this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); - - /// - public void Dispose() - { - this.manager?.FreeFontHandle(this); - this.manager = null; - this.Disposed?.InvokeSafely(this); - this.ImFontChanged = null; - } - - /// - public IFontHandle.ImFontLocked Lock() - { - IFontHandleSubstance? prevSubstance = default; - while (true) - { - var substance = this.ManagerNotDisposed.Substance; - if (substance is null) - throw new InvalidOperationException(); - if (substance == prevSubstance) - throw new ObjectDisposedException(nameof(DelegateFontHandle)); - - prevSubstance = substance; - try - { - substance.DataRoot.AddRef(); - } - catch (ObjectDisposedException) - { - continue; - } - - try - { - var fontPtr = substance.GetFontPtr(this); - if (fontPtr.IsNull()) - continue; - return new(fontPtr, substance.DataRoot); - } - finally - { - substance.DataRoot.Release(); - } - } - } - - /// - public IDisposable Push() - { - ThreadSafety.AssertMainThread(); - var cumulativePresentCalls = Service.GetNullable()?.CumulativePresentCalls ?? 0L; - if (this.lastCumulativePresentCalls != cumulativePresentCalls) - { - this.lastCumulativePresentCalls = cumulativePresentCalls; - if (this.pushedFonts.Count > 0) - { - Log.Warning( - $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + - $"You might be missing a call to {nameof(this.Pop)}."); - this.pushedFonts.Clear(); - } - } - - var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available); - this.pushedFonts.Add(rented); - return rented; - } - - /// - public void Pop() - { - ThreadSafety.AssertMainThread(); - this.pushedFonts[^1].Dispose(); - } - - /// - public Task WaitAsync() - { - if (this.Available) - return Task.FromResult(this); - - var tcs = new TaskCompletionSource(); - this.ImFontChanged += OnImFontChanged; - this.Disposed += OnImFontChanged; - if (this.Available) - OnImFontChanged(this); - return tcs.Task; - - void OnImFontChanged(IFontHandle unused) - { - if (tcs.Task.IsCompletedSuccessfully) - return; - - this.ImFontChanged -= OnImFontChanged; - this.Disposed -= OnImFontChanged; - if (this.manager is null) - tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); - else - tcs.SetResult(this); - } - } - /// public override string ToString() => $"{nameof(GamePrebakedFontHandle)}({this.FontStyle})"; @@ -305,7 +179,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal return; foreach (var handle in hs.RelevantHandles) - handle.ImFontChanged?.InvokeSafely(handle); + handle.InvokeImFontChanged(); } /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs index 3f7255386..0642e7be1 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs @@ -28,17 +28,14 @@ internal sealed class SimplePushedFont : IDisposable ///
/// The -private stack. /// The font pointer being pushed. - /// Whether to push. /// this. - public static SimplePushedFont Rent(List stack, ImFontPtr fontPtr, bool push) + public static SimplePushedFont Rent(List stack, ImFontPtr fontPtr) { - push &= !fontPtr.IsNull(); - var rented = Pool.Get(); Debug.Assert(rented.font.IsNull(), "Rented object must not have its font set"); rented.stack = stack; - if (push) + if (fontPtr.IsNotNullAndLoaded()) { rented.font = fontPtr; ImGui.PushFont(fontPtr); diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 1134704ee..ea3803f35 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -498,7 +498,7 @@ public sealed class UiBuilder : IDisposable [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( - (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), + (GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style), Service.Get()); /// From fb8beb9370865652cf96fb57975d85885a954dbe Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 22:09:47 +0900 Subject: [PATCH 55/71] Move PostPromotion modification functions to PostBuild These changes are done to ensure that `IFontHandle.Lock` will be guaranteed to obtain a fully built font that will not be modified any further (unless `PostPromotion` is being used for modifying fonts, which should not be done by clients.) * Moved `CopyGlyphsAcrossFonts` and `BuildLookupTable` from `PostPromotion` to `PostBuild` build toolkit. * `IFontAtlasBuildToolkit`: Added `GetFont` to enable retrieving font corresponding to a handle being built. * `InterfaceManager`: Use `OnPostBuild` for copying glyphs from Mono to Default. * `FontAtlasBuildStep`: * Removed `Invalid` to prevent an unnecessary switch-case warnings. * Added contracts on when `IFontAtlas.BuildStepChanged` will be called. --- .../Interface/Internal/InterfaceManager.cs | 40 +++---- .../ManagedFontAtlas/FontAtlasBuildStep.cs | 19 +-- .../IFontAtlasBuildToolkit.cs | 8 ++ .../IFontAtlasBuildToolkitPostBuild.cs | 24 ++++ .../IFontAtlasBuildToolkitPostPromotion.cs | 26 +--- .../FontAtlasFactory.BuildToolkit.cs | 112 +++++++++++------- .../FontAtlasFactory.Implementation.cs | 85 ++++++------- Dalamud/Interface/UiBuilder.cs | 2 + 8 files changed, 180 insertions(+), 136 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 25baa5e29..93050d67a 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -717,28 +717,28 @@ internal class InterfaceManager : IDisposable, IServiceType tk => tk.AddDalamudAssetFont( DalamudAsset.InconsolataRegular, new() { SizePx = DefaultFontSizePx }))); - this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( - tk => - { - // Note: the first call of this function is done outside the main thread; this is expected. - // Do not use DefaultFont, IconFont, and MonoFont. - // Use font handles directly. - - using var defaultFont = this.DefaultFontHandle.Lock(); - using var monoFont = this.MonoFontHandle.Lock(); - - // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(defaultFont, monoFont, true); - - // Update default font - unsafe + this.dalamudAtlas.BuildStepChange += e => e + .OnPostBuild( + tk => { - ImGui.GetIO().NativePtr->FontDefault = defaultFont; - } + // Fill missing glyphs in MonoFont from DefaultFont. + tk.CopyGlyphsAcrossFonts( + tk.GetFont(this.DefaultFontHandle), + tk.GetFont(this.MonoFontHandle), + missingOnly: true); + }) + .OnPostPromotion( + tk => + { + // Update the ImGui default font. + unsafe + { + ImGui.GetIO().NativePtr->FontDefault = tk.GetFont(this.DefaultFontHandle); + } - // Broadcast to auto-rebuilding instances - this.AfterBuildFonts?.Invoke(); - }); + // Broadcast to auto-rebuilding instances. + this.AfterBuildFonts?.Invoke(); + }); } // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs index 345ab729d..ba94db435 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs @@ -7,32 +7,35 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// public enum FontAtlasBuildStep { - /// - /// An invalid value. This should never be passed through event callbacks. - /// - Invalid, + // Note: leave 0 alone; make default(FontAtlasBuildStep) not have a valid value /// /// Called before calling .
- /// Expect to be passed. + /// Expect to be passed.
+ /// When called from , this will be called before the delegates + /// passed to . ///
- PreBuild, + PreBuild = 1, /// /// Called after calling .
/// Expect to be passed.
+ /// When called from , this will be called after the delegates + /// passed to ; you can do cross-font operations here.
///
/// This callback is not guaranteed to happen after , /// but it will never happen on its own. ///
- PostBuild, + PostBuild = 2, /// /// Called after promoting staging font atlas to the actual atlas for .
/// Expect to be passed.
+ /// When called from , this will be called after the delegates + /// passed to ; you should not make modifications to fonts.
///
/// This callback is not guaranteed to happen after , /// but it will never happen on its own. ///
- PostPromotion, + PostPromotion = 3, } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs index a997c48c1..f75ed4686 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -80,4 +80,12 @@ public interface IFontAtlasBuildToolkit ///
/// The action to run on dispose. void DisposeWithAtlas(Action action); + + /// + /// Gets the instance of corresponding to + /// from . + /// + /// The font handle. + /// The corresonding , or default if not found. + ImFontPtr GetFont(IFontHandle fontHandle); } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs index 3c14197e0..eb7c7e08c 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -23,4 +23,28 @@ public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit /// Dispose the wrap on error. /// The texture index. int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError); + + /// + /// Copies glyphs across fonts, in a safer way.
+ /// If the font does not belong to the current atlas, this function is a no-op. + ///
+ /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE'); + + /// + /// Calls , with some fixups. + /// + /// The font. + void BuildLookupTable(ImFontPtr font); } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs index 8c3c91624..930851fc7 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs @@ -1,5 +1,3 @@ -using ImGuiNET; - namespace Dalamud.Interface.ManagedFontAtlas; /// @@ -7,27 +5,5 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit { - /// - /// Copies glyphs across fonts, in a safer way.
- /// If the font does not belong to the current atlas, this function is a no-op. - ///
- /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - /// Low codepoint range to copy. - /// High codepoing range to copy. - void CopyGlyphsAcrossFonts( - ImFontPtr source, - ImFontPtr target, - bool missingOnly, - bool rebuildLookupTable = true, - char rangeLow = ' ', - char rangeHigh = '\uFFFE'); - - /// - /// Calls , with some fixups. - /// - /// The font. - void BuildLookupTable(ImFontPtr font); + // empty } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index fde115c9e..3addfabe8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -135,6 +135,19 @@ internal sealed partial class FontAtlasFactory } } + /// + public ImFontPtr GetFont(IFontHandle fontHandle) + { + foreach (var s in this.data.Substances) + { + var f = s.GetFontPtr(fontHandle); + if (!f.IsNull()) + return f; + } + + return default; + } + /// public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) { @@ -608,49 +621,6 @@ internal sealed partial class FontAtlasFactory ArrayPool.Shared.Return(buf); } } - } - - /// - /// Implementations for . - /// - private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion - { - private readonly FontAtlasBuiltData builtData; - - /// - /// Initializes a new instance of the class. - /// - /// The built data. - public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; - - /// - public ImFontPtr Font { get; set; } - - /// - public float Scale => this.builtData.Scale; - - /// - public bool IsAsyncBuildOperation => true; - - /// - public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; - - /// - public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; - - /// - public unsafe ImVectorWrapper Fonts => new( - &this.NewImAtlas.NativePtr->Fonts, - x => ImGuiNative.ImFont_destroy(x->NativePtr)); - - /// - public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); - - /// - public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); - - /// - public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); /// public unsafe void CopyGlyphsAcrossFonts( @@ -707,4 +677,60 @@ internal sealed partial class FontAtlasFactory } } } + + /// + /// Implementations for . + /// + private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion + { + private readonly FontAtlasBuiltData builtData; + + /// + /// Initializes a new instance of the class. + /// + /// The built data. + public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.builtData.Scale; + + /// + public bool IsAsyncBuildOperation => true; + + /// + public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; + + /// + public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; + + /// + public unsafe ImVectorWrapper Fonts => new( + &this.NewImAtlas.NativePtr->Fonts, + x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); + + /// + public ImFontPtr GetFont(IFontHandle fontHandle) + { + foreach (var s in this.builtData.Substances) + { + var f = s.GetFontPtr(fontHandle); + if (!f.IsNull()) + return f; + } + + return default; + } + } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 99ce8dab9..85f7219b2 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reactive.Disposables; -using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.GameFonts; @@ -168,7 +167,7 @@ internal sealed partial class FontAtlasFactory _ => throw new InvalidOperationException(), }; - public unsafe int Release() + public int Release() { switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount)) { @@ -176,22 +175,35 @@ internal sealed partial class FontAtlasFactory return newRefCount; case IRefCountable.RefCountResult.FinalRelease: - if (this.IsBuildInProgress) - { - Log.Error( - "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + - "Stack:\n{trace}", - this.Owner?.Name ?? "", - (nint)this.Atlas.NativePtr, - new StackTrace()); - while (this.IsBuildInProgress) - Thread.Sleep(100); - } - #if VeryVerboseLog Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); #endif - this.Garbage.Dispose(); + + if (this.IsBuildInProgress) + { + unsafe + { + Log.Error( + "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; disposing later.\n" + + "Stack:\n{trace}", + this.Owner?.Name ?? "", + (nint)this.Atlas.NativePtr, + new StackTrace()); + } + + Task.Run( + async () => + { + while (this.IsBuildInProgress) + await Task.Delay(100); + this.Garbage.Dispose(); + }); + } + else + { + this.Garbage.Dispose(); + } + return newRefCount; case IRefCountable.RefCountResult.AlreadyDisposed: @@ -549,20 +561,10 @@ internal sealed partial class FontAtlasFactory return; } - var toolkit = new BuildToolkitPostPromotion(data); + foreach (var substance in data.Substances) + substance.Manager.InvokeFontHandleImFontChanged(); - try - { - this.BuildStepChange?.Invoke(toolkit); - } - catch (Exception e) - { - Log.Error( - e, - "[{name}] {delegateName} PostPromotion error", - this.Name, - nameof(FontAtlasBuildStepDelegate)); - } + var toolkit = new BuildToolkitPostPromotion(data); foreach (var substance in data.Substances) { @@ -580,20 +582,18 @@ internal sealed partial class FontAtlasFactory } } - foreach (var font in toolkit.Fonts) + try { - try - { - toolkit.BuildLookupTable(font); - } - catch (Exception e) - { - Log.Error(e, "[{name}] BuildLookupTable error", this.Name); - } + this.BuildStepChange?.Invoke(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {delegateName} PostPromotion error", + this.Name, + nameof(FontAtlasBuildStepDelegate)); } - - foreach (var substance in data.Substances) - substance.Manager.InvokeFontHandleImFontChanged(); #if VeryVerboseLog Log.Verbose("[{name}] Built from {source}.", this.Name, source); @@ -709,6 +709,9 @@ internal sealed partial class FontAtlasFactory toolkit.PostBuildSubstances(); this.BuildStepChange?.Invoke(toolkit); + foreach (var font in toolkit.Fonts) + toolkit.BuildLookupTable(font); + if (this.factory.SceneTask is { IsCompleted: false } sceneTask) { Log.Verbose( @@ -754,6 +757,8 @@ internal sealed partial class FontAtlasFactory } finally { + // RS is being dumb + // ReSharper disable once ConstantConditionalAccessQualifier toolkit?.Dispose(); this.buildQueued = false; } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index ea3803f35..af4cc39c2 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -700,6 +700,8 @@ public sealed class UiBuilder : IDisposable if (e.IsAsyncBuildOperation) return; + ThreadSafety.AssertMainThread(); + if (this.BuildFonts is not null) { e.OnPreBuild( From 871deca6e936e0528bb8e4cbd6031935a3099dd3 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 23:00:47 +0900 Subject: [PATCH 56/71] Remove PostPromotion event `PostPromotion` is removed, as `IFontHandle.ImFontChanged` now does the job. It also removes the possibility that resources may get disposed while post promotion callback is in progress. * `IFontHandle.ImFontChanged` is now called with a locked instance of the font. * `IFontHandle.ImFontLocked`: Added `NewRef` to increase reference count. --- Dalamud.CorePlugin/PluginImpl.cs | 2 +- Dalamud/Interface/GameFonts/GameFontHandle.cs | 2 +- .../Interface/Internal/InterfaceManager.cs | 46 ++++++------ .../ManagedFontAtlas/FontAtlasBuildStep.cs | 11 --- .../FontAtlasBuildStepDelegate.cs | 7 +- .../FontAtlasBuildToolkitUtilities.cs | 17 ----- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 6 +- .../IFontAtlasBuildToolkitPostPromotion.cs | 9 --- .../Interface/ManagedFontAtlas/IFontHandle.cs | 29 ++++++-- .../Internals/DelegateFontHandle.cs | 40 +---------- .../FontAtlasFactory.BuildToolkit.cs | 56 --------------- .../FontAtlasFactory.Implementation.cs | 71 +++++-------------- .../ManagedFontAtlas/Internals/FontHandle.cs | 66 ++++++++++++----- .../Internals/GamePrebakedFontHandle.cs | 19 +---- .../Internals/IFontHandleManager.cs | 5 -- .../Internals/IFontHandleSubstance.cs | 16 ++--- Dalamud/Interface/UiBuilder.cs | 5 +- 17 files changed, 138 insertions(+), 269 deletions(-) delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 96d212dd3..cb9b4368a 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -69,7 +69,7 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi; - this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += fc => + this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += (fc, _) => { Log.Information($"CorePlugin : DefaultFontHandle.ImFontChanged called {fc}"); }; diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 4c472c032..679452ba4 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -31,7 +31,7 @@ public sealed class GameFontHandle : IFontHandle } /// - public event Action ImFontChanged + public event IFontHandle.ImFontChangedDelegate ImFontChanged { add => this.fontHandle.ImFontChanged += value; remove => this.fontHandle.ImFontChanged -= value; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 93050d67a..18bb95799 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -79,6 +79,7 @@ internal class InterfaceManager : IDisposable, IServiceType private Hook? resizeBuffersHook; private IFontAtlas? dalamudAtlas; + private IFontHandle.ImFontLocked defaultFontResourceLock; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -717,32 +718,35 @@ internal class InterfaceManager : IDisposable, IServiceType tk => tk.AddDalamudAssetFont( DalamudAsset.InconsolataRegular, new() { SizePx = DefaultFontSizePx }))); - this.dalamudAtlas.BuildStepChange += e => e - .OnPostBuild( - tk => + this.dalamudAtlas.BuildStepChange += e => e.OnPostBuild( + tk => + { + // Fill missing glyphs in MonoFont from DefaultFont. + tk.CopyGlyphsAcrossFonts( + tk.GetFont(this.DefaultFontHandle), + tk.GetFont(this.MonoFontHandle), + missingOnly: true); + }); + this.DefaultFontHandle.ImFontChanged += (_, font) => Service.Get().RunOnFrameworkThread( + () => + { + // Update the ImGui default font. + unsafe { - // Fill missing glyphs in MonoFont from DefaultFont. - tk.CopyGlyphsAcrossFonts( - tk.GetFont(this.DefaultFontHandle), - tk.GetFont(this.MonoFontHandle), - missingOnly: true); - }) - .OnPostPromotion( - tk => - { - // Update the ImGui default font. - unsafe - { - ImGui.GetIO().NativePtr->FontDefault = tk.GetFont(this.DefaultFontHandle); - } + ImGui.GetIO().NativePtr->FontDefault = font; + } - // Broadcast to auto-rebuilding instances. - this.AfterBuildFonts?.Invoke(); - }); + // Update the reference to the resources of the default font. + this.defaultFontResourceLock.Dispose(); + this.defaultFontResourceLock = font.NewRef(); + + // Broadcast to auto-rebuilding instances. + this.AfterBuildFonts?.Invoke(); + }); } // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. - _ = this.dalamudAtlas.BuildFontsAsync(false); + _ = this.dalamudAtlas.BuildFontsAsync(); this.address.Setup(sigScanner); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs index ba94db435..dcfcc32e3 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs @@ -27,15 +27,4 @@ public enum FontAtlasBuildStep /// but it will never happen on its own. /// PostBuild = 2, - - /// - /// Called after promoting staging font atlas to the actual atlas for .
- /// Expect to be passed.
- /// When called from , this will be called after the delegates - /// passed to ; you should not make modifications to fonts.
- ///
- /// This callback is not guaranteed to happen after , - /// but it will never happen on its own. - ///
- PostPromotion = 3, } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs index 4f5b34061..2ed88102f 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs @@ -6,10 +6,9 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// A toolkit that may help you for font building steps. /// /// An implementation of may implement all of -/// , , and -/// .
+/// and .
/// Either use to identify the build step, or use -/// , , -/// and for routing. +/// and +/// for routing. ///
public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs index 586887a3b..4c3e9023a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -113,21 +113,4 @@ public static class FontAtlasBuildToolkitUtilities action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit); return toolkit; } - - /// - /// Invokes - /// if of - /// is . - /// - /// The toolkit. - /// The action. - /// toolkit, for method chaining. - public static IFontAtlasBuildToolkit OnPostPromotion( - this IFontAtlasBuildToolkit toolkit, - Action action) - { - if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion) - action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit); - return toolkit; - } } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index 491292f9d..a9c21f94e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -129,8 +129,7 @@ public interface IFontAtlas : IDisposable void BuildFontsOnNextFrame(); /// - /// Rebuilds fonts immediately, on the current thread.
- /// Even the callback for will be called on the same thread. + /// Rebuilds fonts immediately, on the current thread. ///
/// If is . void BuildFontsImmediately(); @@ -138,8 +137,7 @@ public interface IFontAtlas : IDisposable /// /// Rebuilds fonts asynchronously, on any thread. /// - /// Call on the main thread. /// The task. /// If is . - Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); + Task BuildFontsAsync(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs deleted file mode 100644 index 930851fc7..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Toolkit for use when the build state is . -/// -public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit -{ - // empty -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 97d345925..d58a89e56 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -12,14 +12,17 @@ namespace Dalamud.Interface.ManagedFontAtlas; public interface IFontHandle : IDisposable { /// - /// Called when the built instance of has been changed.
- /// This event will be invoked on the same thread with - /// ., - /// when the build step is .
- /// See , , and - /// . + /// Delegate for . ///
- event Action ImFontChanged; + /// The relevant font handle. + /// The locked font for this font handle, locked during the call of this delegate. + public delegate void ImFontChangedDelegate(IFontHandle fontHandle, ImFontLocked lockedFont); + + /// + /// Called when the built instance of has been changed.
+ /// This event can be invoked outside the main thread. + ///
+ event ImFontChangedDelegate ImFontChanged; /// /// Gets the load exception, if it failed to load. Otherwise, it is null. @@ -102,6 +105,18 @@ public interface IFontHandle : IDisposable public static unsafe implicit operator ImFont*(ImFontLocked l) => l.ImFont.NativePtr; + /// + /// Creates a new instance of with an additional reference to the owner. + /// + /// The new locked instance. + public readonly ImFontLocked NewRef() + { + if (this.owner is null) + throw new ObjectDisposedException(nameof(ImFontLocked)); + this.owner.AddRef(); + return new(this.ImFont, this.owner); + } + /// public void Dispose() { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index 53a836511..b13c60a53 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -80,16 +80,6 @@ internal sealed class DelegateFontHandle : FontHandle this.handles.Remove(cgfh); } - /// - public void InvokeFontHandleImFontChanged() - { - if (this.Substance is not HandleSubstance hs) - return; - - foreach (var handle in hs.RelevantHandles) - handle.InvokeImFontChanged(); - } - /// public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { @@ -133,6 +123,9 @@ internal sealed class DelegateFontHandle : FontHandle // Not owned by this class. Do not dispose. public DelegateFontHandle[] RelevantHandles { get; } + /// + ICollection IFontHandleSubstance.RelevantHandles => this.RelevantHandles; + /// public IRefCountable DataRoot { get; } @@ -306,32 +299,5 @@ internal sealed class DelegateFontHandle : FontHandle } } } - - /// - public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) - { - foreach (var k in this.RelevantHandles) - { - if (!this.fonts[k].IsNotNullAndLoaded()) - continue; - - try - { - toolkitPostPromotion.Font = this.fonts[k]; - k.CallOnBuildStepChange.Invoke(toolkitPostPromotion); - } - catch (Exception e) - { - this.fonts[k] = default; - this.buildExceptions[k] = e; - - Log.Error( - e, - "[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.", - this.Manager.Name, - nameof(FontAtlasBuildStepDelegate)); - } - } - } } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 3addfabe8..e2b096701 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -677,60 +677,4 @@ internal sealed partial class FontAtlasFactory } } } - - /// - /// Implementations for . - /// - private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion - { - private readonly FontAtlasBuiltData builtData; - - /// - /// Initializes a new instance of the class. - /// - /// The built data. - public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; - - /// - public ImFontPtr Font { get; set; } - - /// - public float Scale => this.builtData.Scale; - - /// - public bool IsAsyncBuildOperation => true; - - /// - public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; - - /// - public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; - - /// - public unsafe ImVectorWrapper Fonts => new( - &this.NewImAtlas.NativePtr->Fonts, - x => ImGuiNative.ImFont_destroy(x->NativePtr)); - - /// - public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); - - /// - public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); - - /// - public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); - - /// - public ImFontPtr GetFont(IFontHandle fontHandle) - { - foreach (var s in this.builtData.Substances) - { - var f = s.GetFontPtr(fontHandle); - if (!f.IsNull()) - return f; - } - - return default; - } - } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 85f7219b2..4e98bf226 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -486,7 +486,7 @@ internal sealed partial class FontAtlasFactory } /// - public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true) + public Task BuildFontsAsync() { #if VeryVerboseLog Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync)); @@ -519,15 +519,7 @@ internal sealed partial class FontAtlasFactory if (res.Atlas.IsNull()) return res; - if (callPostPromotionOnMainThread) - { - await this.factory.Framework.RunOnFrameworkThread( - () => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync))); - } - else - { - this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); - } + this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); return res; } @@ -536,6 +528,10 @@ internal sealed partial class FontAtlasFactory private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) { + // Capture the locks inside the lock block, so that the fonts are guaranteed to be the ones just built. + var fontsAndLocks = new List<(FontHandle FontHandle, IFontHandle.ImFontLocked Lock)>(); + using var garbage = new DisposeSafety.ScopedFinalizer(); + lock (this.syncRoot) { if (this.buildIndex != rebuildIndex) @@ -549,56 +545,25 @@ internal sealed partial class FontAtlasFactory prevBuiltData.ExplicitDisposeIgnoreExceptions(); this.buildTask = EmptyTask; + fontsAndLocks.EnsureCapacity(data.Substances.Sum(x => x.RelevantHandles.Count)); foreach (var substance in data.Substances) + { substance.Manager.Substance = substance; + foreach (var fontHandle in substance.RelevantHandles) + { + substance.DataRoot.AddRef(); + var locked = new IFontHandle.ImFontLocked(substance.GetFontPtr(fontHandle), substance.DataRoot); + fontsAndLocks.Add((fontHandle, garbage.Add(locked))); + } + } } - lock (this.syncRootPostPromotion) - { - if (this.buildIndex != rebuildIndex) - { - data.ExplicitDisposeIgnoreExceptions(); - return; - } - - foreach (var substance in data.Substances) - substance.Manager.InvokeFontHandleImFontChanged(); - - var toolkit = new BuildToolkitPostPromotion(data); - - foreach (var substance in data.Substances) - { - try - { - substance.OnPostPromotion(toolkit); - } - catch (Exception e) - { - Log.Error( - e, - "[{name}] {substance} PostPromotion error", - this.Name, - substance.GetType().FullName ?? substance.GetType().Name); - } - } - - try - { - this.BuildStepChange?.Invoke(toolkit); - } - catch (Exception e) - { - Log.Error( - e, - "[{name}] {delegateName} PostPromotion error", - this.Name, - nameof(FontAtlasBuildStepDelegate)); - } + foreach (var (fontHandle, lockedFont) in fontsAndLocks) + fontHandle.InvokeImFontChanged(lockedFont); #if VeryVerboseLog - Log.Verbose("[{name}] Built from {source}.", this.Name, source); + Log.Verbose("[{name}] Built from {source}.", this.Name, source); #endif - } } private void ImGuiSceneOnNewRenderFrame() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 93b17f86e..d01b0df87 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -41,12 +41,12 @@ internal abstract class FontHandle : IFontHandle } /// - public event Action? ImFontChanged; + public event IFontHandle.ImFontChangedDelegate? ImFontChanged; /// /// Event to be called on the first call. /// - protected event Action? Disposed; + protected event Action? Disposed; /// public Exception? LoadException => this.Manager.Substance?.GetBuildException(this); @@ -70,6 +70,22 @@ internal abstract class FontHandle : IFontHandle GC.SuppressFinalize(this); } + /// + /// Invokes . + /// + /// The font, locked during the call of . + public void InvokeImFontChanged(IFontHandle.ImFontLocked font) + { + try + { + this.ImFontChanged?.Invoke(this, font); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.InvokeImFontChanged)}: error"); + } + } + /// /// Obtains an instance of corresponding to this font handle, /// to be released after rendering the current frame. @@ -220,35 +236,51 @@ internal abstract class FontHandle : IFontHandle var tcs = new TaskCompletionSource(); this.ImFontChanged += OnImFontChanged; - this.Disposed += OnImFontChanged; + this.Disposed += OnDisposed; if (this.Available) - OnImFontChanged(this); + OnImFontChanged(this, default); return tcs.Task; - void OnImFontChanged(IFontHandle unused) + void OnImFontChanged(IFontHandle unused, IFontHandle.ImFontLocked unused2) { if (tcs.Task.IsCompletedSuccessfully) return; this.ImFontChanged -= OnImFontChanged; - this.Disposed -= OnImFontChanged; - if (this.manager is null) - tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); - else + this.Disposed -= OnDisposed; + try + { tcs.SetResult(this); + } + catch + { + // ignore + } + } + + void OnDisposed() + { + if (tcs.Task.IsCompletedSuccessfully) + return; + + this.ImFontChanged -= OnImFontChanged; + this.Disposed -= OnDisposed; + try + { + tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); + } + catch + { + // ignore + } } } /// - /// Invokes . - /// - protected void InvokeImFontChanged() => this.ImFontChanged.InvokeSafely(this); - - /// - /// Overrideable implementation for . + /// Implementation for . /// /// If true, then the function is being called from . - protected virtual void Dispose(bool disposing) + protected void Dispose(bool disposing) { if (disposing) { @@ -256,7 +288,7 @@ internal abstract class FontHandle : IFontHandle Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); this.Manager.FreeFontHandle(this); this.manager = null; - this.Disposed?.InvokeSafely(this); + this.Disposed?.InvokeSafely(); this.ImFontChanged = null; } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index e062405b8..b6c9817aa 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -172,16 +172,6 @@ internal class GamePrebakedFontHandle : FontHandle } } - /// - public void InvokeFontHandleImFontChanged() - { - if (this.Substance is not HandleSubstance hs) - return; - - foreach (var handle in hs.RelevantHandles) - handle.InvokeImFontChanged(); - } - /// public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { @@ -232,6 +222,9 @@ internal class GamePrebakedFontHandle : FontHandle // Not owned by this class. Do not dispose. public GamePrebakedFontHandle[] RelevantHandles { get; } + /// + ICollection IFontHandleSubstance.RelevantHandles => this.RelevantHandles; + /// public IRefCountable DataRoot { get; } @@ -413,12 +406,6 @@ internal class GamePrebakedFontHandle : FontHandle } } - /// - public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) - { - // Irrelevant - } - /// /// Creates a new template font. /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs index 7066817b7..94976598a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -32,9 +32,4 @@ internal interface IFontHandleManager : IDisposable /// The data root. /// The new substance. IFontHandleSubstance NewSubstance(IRefCountable dataRoot); - - /// - /// Invokes . - /// - void InvokeFontHandleImFontChanged(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs index 73c14efc1..62c893a48 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -1,4 +1,6 @@ -using Dalamud.Utility; +using System.Collections.Generic; + +using Dalamud.Utility; using ImGuiNET; @@ -32,6 +34,11 @@ internal interface IFontHandleSubstance : IDisposable [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] bool CreateFontOnAccess { get; set; } + /// + /// Gets the relevant handles. + /// + public ICollection RelevantHandles { get; } + /// /// Gets the font. /// @@ -64,11 +71,4 @@ internal interface IFontHandleSubstance : IDisposable /// /// The toolkit. void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild); - - /// - /// Called on the specific thread depending on after - /// promoting the staging atlas to direct use with . - /// - /// The toolkit. - void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion); } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index af4cc39c2..b038d44ba 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -744,7 +744,7 @@ public sealed class UiBuilder : IDisposable this.wrapped.ImFontChanged += this.WrappedOnImFontChanged; } - public event Action? ImFontChanged; + public event IFontHandle.ImFontChangedDelegate? ImFontChanged; public Exception? LoadException => this.wrapped!.LoadException ?? new ObjectDisposedException(nameof(FontHandleWrapper)); @@ -775,6 +775,7 @@ public sealed class UiBuilder : IDisposable public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped})"; - private void WrappedOnImFontChanged(IFontHandle obj) => this.ImFontChanged.InvokeSafely(this); + private void WrappedOnImFontChanged(IFontHandle obj, IFontHandle.ImFontLocked lockedFont) => + this.ImFontChanged?.Invoke(obj, lockedFont); } } From df89472d4ccf5cc903fd5009e7caad1251dc04dd Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 23:18:18 +0900 Subject: [PATCH 57/71] Consistent BuildTask resolution timing `BuildFontsImmediately` and `BuildFontsAsync` set `BuildTask` to completion at different point of build process, and changed the code to make it consistent that `BuildTask` is set to completion after `PromoteBuiltData` returns. --- .../FontAtlasFactory.Implementation.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 4e98bf226..7fadf669d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reactive.Disposables; +using System.Runtime.ExceptionServices; +using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.GameFonts; @@ -229,7 +231,6 @@ internal sealed partial class FontAtlasFactory private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager; private readonly IFontHandleManager[] fontHandleManagers; - private readonly object syncRootPostPromotion = new(); private readonly object syncRoot = new(); private Task buildTask = EmptyTask; @@ -449,10 +450,9 @@ internal sealed partial class FontAtlasFactory } var tcs = new TaskCompletionSource(); - int rebuildIndex; try { - rebuildIndex = ++this.buildIndex; + var rebuildIndex = Interlocked.Increment(ref this.buildIndex); lock (this.syncRoot) { if (!this.buildTask.IsCompleted) @@ -469,11 +469,18 @@ internal sealed partial class FontAtlasFactory var r = this.RebuildFontsPrivate(false, scale); r.Wait(); if (r.IsCompletedSuccessfully) + { + this.PromoteBuiltData(rebuildIndex, r.Result, nameof(this.BuildFontsImmediately)); tcs.SetResult(r.Result); - else if (r.Exception is not null) - tcs.SetException(r.Exception); + } + else if ((r.Exception?.InnerException ?? r.Exception) is { } taskException) + { + ExceptionDispatchInfo.Capture(taskException).Throw(); + } else - tcs.SetCanceled(); + { + throw new OperationCanceledException(); + } } catch (Exception e) { @@ -481,8 +488,6 @@ internal sealed partial class FontAtlasFactory Log.Error(e, "[{name}] Failed to build fonts.", this.Name); throw; } - - this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately)); } /// @@ -503,7 +508,7 @@ internal sealed partial class FontAtlasFactory lock (this.syncRoot) { var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; - var rebuildIndex = ++this.buildIndex; + var rebuildIndex = Interlocked.Increment(ref this.buildIndex); return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); async Task BuildInner(Task unused) @@ -519,14 +524,14 @@ internal sealed partial class FontAtlasFactory if (res.Atlas.IsNull()) return res; - this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); + this.PromoteBuiltData(rebuildIndex, res, nameof(this.BuildFontsAsync)); return res; } } } - private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) + private void PromoteBuiltData(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) { // Capture the locks inside the lock block, so that the fonts are guaranteed to be the ones just built. var fontsAndLocks = new List<(FontHandle FontHandle, IFontHandle.ImFontLocked Lock)>(); From 68dc16803c0a23993aa89193dd99cbdc73e14940 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 23:39:25 +0900 Subject: [PATCH 58/71] Turn ImFontLocked into a class As `ImFontLocked` utilizes a reference counter, changed it to a class so that at worst case we still got the destructor to decrease the reference count. --- .../Interface/Internal/InterfaceManager.cs | 34 ++++++---- .../Interface/ManagedFontAtlas/IFontHandle.cs | 67 ++++++++++++++----- .../FontAtlasFactory.Implementation.cs | 4 +- .../ManagedFontAtlas/Internals/FontHandle.cs | 2 +- .../Internals/SimplePushedFont.cs | 2 +- 5 files changed, 75 insertions(+), 34 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 18bb95799..82299a136 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -79,7 +79,7 @@ internal class InterfaceManager : IDisposable, IServiceType private Hook? resizeBuffersHook; private IFontAtlas? dalamudAtlas; - private IFontHandle.ImFontLocked defaultFontResourceLock; + private IFontHandle.ImFontLocked? defaultFontResourceLock; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -243,6 +243,8 @@ internal class InterfaceManager : IDisposable, IServiceType Disposer(); this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + this.defaultFontResourceLock?.Dispose(); // lock outlives handle and atlas + this.defaultFontResourceLock = null; this.dalamudAtlas?.Dispose(); this.scene?.Dispose(); return; @@ -727,22 +729,26 @@ internal class InterfaceManager : IDisposable, IServiceType tk.GetFont(this.MonoFontHandle), missingOnly: true); }); - this.DefaultFontHandle.ImFontChanged += (_, font) => Service.Get().RunOnFrameworkThread( - () => - { - // Update the ImGui default font. - unsafe + this.DefaultFontHandle.ImFontChanged += (_, font) => + { + var fontLocked = font.NewRef(); + Service.Get().RunOnFrameworkThread( + () => { - ImGui.GetIO().NativePtr->FontDefault = font; - } + // Update the ImGui default font. + unsafe + { + ImGui.GetIO().NativePtr->FontDefault = fontLocked; + } - // Update the reference to the resources of the default font. - this.defaultFontResourceLock.Dispose(); - this.defaultFontResourceLock = font.NewRef(); + // Update the reference to the resources of the default font. + this.defaultFontResourceLock?.Dispose(); + this.defaultFontResourceLock = fontLocked; - // Broadcast to auto-rebuilding instances. - this.AfterBuildFonts?.Invoke(); - }); + // Broadcast to auto-rebuilding instances. + this.AfterBuildFonts?.Invoke(); + }); + }; } // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index d58a89e56..dd3775236 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -1,9 +1,14 @@ -using System.Threading.Tasks; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; +using Microsoft.Extensions.ObjectPool; + namespace Dalamud.Interface.ManagedFontAtlas; /// @@ -80,26 +85,23 @@ public interface IFontHandle : IDisposable /// The wrapper for , guaranteeing that the associated data will be available as long as /// this struct is not disposed. /// - public struct ImFontLocked : IDisposable + public class ImFontLocked : IDisposable { - /// - /// The associated . - /// - public ImFontPtr ImFont; + // Using constructor instead of DefaultObjectPoolProvider, since we do not want the pool to call Dispose. + private static readonly ObjectPool Pool = + new DefaultObjectPool(new DefaultPooledObjectPolicy()); private IRefCountable? owner; /// - /// Initializes a new instance of the struct. - /// Ownership of reference of is transferred. + /// Finalizes an instance of the class. /// - /// The contained font. - /// The owner. - internal ImFontLocked(ImFontPtr imFont, IRefCountable owner) - { - this.ImFont = imFont; - this.owner = owner; - } + ~ImFontLocked() => this.FreeOwner(); + + /// + /// Gets the associated . + /// + public ImFontPtr ImFont { get; private set; } public static implicit operator ImFontPtr(ImFontLocked l) => l.ImFont; @@ -109,16 +111,47 @@ public interface IFontHandle : IDisposable /// Creates a new instance of with an additional reference to the owner. /// /// The new locked instance. - public readonly ImFontLocked NewRef() + public ImFontLocked NewRef() { if (this.owner is null) throw new ObjectDisposedException(nameof(ImFontLocked)); + + var rented = Pool.Get(); + rented.owner = this.owner; + rented.ImFont = this.ImFont; + this.owner.AddRef(); - return new(this.ImFont, this.owner); + return rented; } /// + [SuppressMessage( + "Usage", + "CA1816:Dispose methods should call SuppressFinalize", + Justification = "Dispose returns this object to the pool.")] public void Dispose() + { + this.FreeOwner(); + Pool.Return(this); + } + + /// + /// Initializes a new instance of the class. + /// Ownership of reference of is transferred. + /// + /// The contained font. + /// The owner. + /// The rented instance of . + internal static ImFontLocked Rent(ImFontPtr font, IRefCountable owner) + { + var rented = Pool.Get(); + Debug.Assert(rented.ImFont.IsNull(), "Rented object must not have its font set"); + rented.ImFont = font; + rented.owner = owner; + return rented; + } + + private void FreeOwner() { if (this.owner is null) return; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 7fadf669d..06bc5b7ab 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -557,7 +557,9 @@ internal sealed partial class FontAtlasFactory foreach (var fontHandle in substance.RelevantHandles) { substance.DataRoot.AddRef(); - var locked = new IFontHandle.ImFontLocked(substance.GetFontPtr(fontHandle), substance.DataRoot); + var locked = IFontHandle.ImFontLocked.Rent( + substance.GetFontPtr(fontHandle), + substance.DataRoot); fontsAndLocks.Add((fontHandle, garbage.Add(locked))); } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index d01b0df87..f8291cc51 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -182,7 +182,7 @@ internal abstract class FontHandle : IFontHandle // Transfer the ownership of reference. errorMessage = null; - return new(fontPtr, substance.DataRoot); + return IFontHandle.ImFontLocked.Rent(fontPtr, substance.DataRoot); } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs index 0642e7be1..0c96025ac 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs @@ -28,7 +28,7 @@ internal sealed class SimplePushedFont : IDisposable /// /// The -private stack. /// The font pointer being pushed. - /// this. + /// The rented instance of . public static SimplePushedFont Rent(List stack, ImFontPtr fontPtr) { var rented = Pool.Get(); From 5161053cb34d9a8299beb982c703fe1782f7a55f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 24 Jan 2024 00:19:27 +0900 Subject: [PATCH 59/71] Move `IFontHandle.ImFontLocked` to `ILockedImFont`+impl --- Dalamud/Interface/GameFonts/GameFontHandle.cs | 2 +- .../Interface/Internal/InterfaceManager.cs | 10 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 2 +- .../Interface/ManagedFontAtlas/IFontHandle.cs | 96 +------------------ .../ManagedFontAtlas/ILockedImFont.cs | 21 ++++ .../FontAtlasFactory.Implementation.cs | 4 +- .../ManagedFontAtlas/Internals/FontHandle.cs | 14 +-- .../Internals/LockedImFont.cs | 62 ++++++++++++ Dalamud/Interface/UiBuilder.cs | 4 +- 9 files changed, 105 insertions(+), 110 deletions(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 679452ba4..2594eea0e 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -71,7 +71,7 @@ public sealed class GameFontHandle : IFontHandle public void Dispose() => this.fontHandle.Dispose(); /// - public IFontHandle.ImFontLocked Lock() => this.fontHandle.Lock(); + public ILockedImFont Lock() => this.fontHandle.Lock(); /// public IDisposable Push() => this.fontHandle.Push(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 82299a136..6cf4a8b90 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -63,7 +63,7 @@ internal class InterfaceManager : IDisposable, IServiceType public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; private readonly ConcurrentBag deferredDisposeTextures = new(); - private readonly ConcurrentBag deferredDisposeImFontLockeds = new(); + private readonly ConcurrentBag deferredDisposeImFontLockeds = new(); [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); @@ -79,7 +79,7 @@ internal class InterfaceManager : IDisposable, IServiceType private Hook? resizeBuffersHook; private IFontAtlas? dalamudAtlas; - private IFontHandle.ImFontLocked? defaultFontResourceLock; + private ILockedImFont? defaultFontResourceLock; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -408,10 +408,10 @@ internal class InterfaceManager : IDisposable, IServiceType } /// - /// Enqueue an to be disposed at the end of the frame. + /// Enqueue an to be disposed at the end of the frame. /// /// The disposable. - public void EnqueueDeferredDispose(in IFontHandle.ImFontLocked locked) + public void EnqueueDeferredDispose(in ILockedImFont locked) { this.deferredDisposeImFontLockeds.Add(locked); } @@ -738,7 +738,7 @@ internal class InterfaceManager : IDisposable, IServiceType // Update the ImGui default font. unsafe { - ImGui.GetIO().NativePtr->FontDefault = fontLocked; + ImGui.GetIO().NativePtr->FontDefault = fontLocked.ImFont; } // Update the reference to the resources of the default font. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index 7b649a895..b486cc7d9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -249,7 +249,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable await handle.WaitAsync(); var locked = handle.Lock(); garbage.Add(locked); - fonts.Add(locked); + fonts.Add(locked.ImFont); } } catch (ObjectDisposedException) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index dd3775236..11c26616b 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -1,14 +1,7 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; - -using Dalamud.Interface.Utility; -using Dalamud.Utility; +using System.Threading.Tasks; using ImGuiNET; -using Microsoft.Extensions.ObjectPool; - namespace Dalamud.Interface.ManagedFontAtlas; /// @@ -21,7 +14,7 @@ public interface IFontHandle : IDisposable /// /// The relevant font handle. /// The locked font for this font handle, locked during the call of this delegate. - public delegate void ImFontChangedDelegate(IFontHandle fontHandle, ImFontLocked lockedFont); + public delegate void ImFontChangedDelegate(IFontHandle fontHandle, ILockedImFont lockedFont); /// /// Called when the built instance of has been changed.
@@ -48,13 +41,13 @@ public interface IFontHandle : IDisposable /// , for use in any thread.
/// Modification of the font will exhibit undefined behavior if some other thread also uses the font. ///
- /// An instance of that must be disposed after use. + /// An instance of that must be disposed after use. /// /// Calling . will not unlock the /// locked by this function. /// /// If is false. - ImFontLocked Lock(); + ILockedImFont Lock(); /// /// Pushes the current font into ImGui font stack, if available.
@@ -80,85 +73,4 @@ public interface IFontHandle : IDisposable ///
/// A task containing this . Task WaitAsync(); - - /// - /// The wrapper for , guaranteeing that the associated data will be available as long as - /// this struct is not disposed. - /// - public class ImFontLocked : IDisposable - { - // Using constructor instead of DefaultObjectPoolProvider, since we do not want the pool to call Dispose. - private static readonly ObjectPool Pool = - new DefaultObjectPool(new DefaultPooledObjectPolicy()); - - private IRefCountable? owner; - - /// - /// Finalizes an instance of the class. - /// - ~ImFontLocked() => this.FreeOwner(); - - /// - /// Gets the associated . - /// - public ImFontPtr ImFont { get; private set; } - - public static implicit operator ImFontPtr(ImFontLocked l) => l.ImFont; - - public static unsafe implicit operator ImFont*(ImFontLocked l) => l.ImFont.NativePtr; - - /// - /// Creates a new instance of with an additional reference to the owner. - /// - /// The new locked instance. - public ImFontLocked NewRef() - { - if (this.owner is null) - throw new ObjectDisposedException(nameof(ImFontLocked)); - - var rented = Pool.Get(); - rented.owner = this.owner; - rented.ImFont = this.ImFont; - - this.owner.AddRef(); - return rented; - } - - /// - [SuppressMessage( - "Usage", - "CA1816:Dispose methods should call SuppressFinalize", - Justification = "Dispose returns this object to the pool.")] - public void Dispose() - { - this.FreeOwner(); - Pool.Return(this); - } - - /// - /// Initializes a new instance of the class. - /// Ownership of reference of is transferred. - /// - /// The contained font. - /// The owner. - /// The rented instance of . - internal static ImFontLocked Rent(ImFontPtr font, IRefCountable owner) - { - var rented = Pool.Get(); - Debug.Assert(rented.ImFont.IsNull(), "Rented object must not have its font set"); - rented.ImFont = font; - rented.owner = owner; - return rented; - } - - private void FreeOwner() - { - if (this.owner is null) - return; - - this.owner.Release(); - this.owner = null; - this.ImFont = default; - } - } } diff --git a/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs b/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs new file mode 100644 index 000000000..9136d2723 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs @@ -0,0 +1,21 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// The wrapper for , guaranteeing that the associated data will be available as long as +/// this struct is not disposed. +/// +public interface ILockedImFont : IDisposable +{ + /// + /// Gets the associated . + /// + ImFontPtr ImFont { get; } + + /// + /// Creates a new instance of with an additional reference to the owner. + /// + /// The new locked instance. + ILockedImFont NewRef(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 06bc5b7ab..4d636b8cf 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -534,7 +534,7 @@ internal sealed partial class FontAtlasFactory private void PromoteBuiltData(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) { // Capture the locks inside the lock block, so that the fonts are guaranteed to be the ones just built. - var fontsAndLocks = new List<(FontHandle FontHandle, IFontHandle.ImFontLocked Lock)>(); + var fontsAndLocks = new List<(FontHandle FontHandle, ILockedImFont Lock)>(); using var garbage = new DisposeSafety.ScopedFinalizer(); lock (this.syncRoot) @@ -557,7 +557,7 @@ internal sealed partial class FontAtlasFactory foreach (var fontHandle in substance.RelevantHandles) { substance.DataRoot.AddRef(); - var locked = IFontHandle.ImFontLocked.Rent( + var locked = new LockedImFont( substance.GetFontPtr(fontHandle), substance.DataRoot); fontsAndLocks.Add((fontHandle, garbage.Add(locked))); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index f8291cc51..47254a5c9 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -74,7 +74,7 @@ internal abstract class FontHandle : IFontHandle /// Invokes . /// /// The font, locked during the call of . - public void InvokeImFontChanged(IFontHandle.ImFontLocked font) + public void InvokeImFontChanged(ILockedImFont font) { try { @@ -133,11 +133,11 @@ internal abstract class FontHandle : IFontHandle /// /// The error message, if any. /// - /// An instance of that must be disposed after use on success; + /// An instance of that must be disposed after use on success; /// null with populated on failure. /// /// Still may be thrown. - public IFontHandle.ImFontLocked? TryLock(out string? errorMessage) + public ILockedImFont? TryLock(out string? errorMessage) { IFontHandleSubstance? prevSubstance = default; while (true) @@ -182,12 +182,12 @@ internal abstract class FontHandle : IFontHandle // Transfer the ownership of reference. errorMessage = null; - return IFontHandle.ImFontLocked.Rent(fontPtr, substance.DataRoot); + return new LockedImFont(fontPtr, substance.DataRoot); } } /// - public IFontHandle.ImFontLocked Lock() => + public ILockedImFont Lock() => this.TryLock(out var errorMessage) ?? throw new InvalidOperationException(errorMessage); /// @@ -238,10 +238,10 @@ internal abstract class FontHandle : IFontHandle this.ImFontChanged += OnImFontChanged; this.Disposed += OnDisposed; if (this.Available) - OnImFontChanged(this, default); + OnImFontChanged(this, null); return tcs.Task; - void OnImFontChanged(IFontHandle unused, IFontHandle.ImFontLocked unused2) + void OnImFontChanged(IFontHandle unused, ILockedImFont? unused2) { if (tcs.Task.IsCompletedSuccessfully) return; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs new file mode 100644 index 000000000..bd50502c8 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs @@ -0,0 +1,62 @@ +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// The implementation for . +/// +internal class LockedImFont : ILockedImFont +{ + private IRefCountable? owner; + + /// + /// Initializes a new instance of the class. + /// Ownership of reference of is transferred. + /// + /// The contained font. + /// The owner. + /// The rented instance of . + internal LockedImFont(ImFontPtr font, IRefCountable owner) + { + this.ImFont = font; + this.owner = owner; + } + + /// + /// Finalizes an instance of the class. + /// + ~LockedImFont() => this.FreeOwner(); + + /// + public ImFontPtr ImFont { get; private set; } + + /// + public ILockedImFont NewRef() + { + if (this.owner is null) + throw new ObjectDisposedException(nameof(LockedImFont)); + + var newRef = new LockedImFont(this.ImFont, this.owner); + this.owner.AddRef(); + return newRef; + } + + /// + public void Dispose() + { + this.FreeOwner(); + GC.SuppressFinalize(this); + } + + private void FreeOwner() + { + if (this.owner is null) + return; + + this.owner.Release(); + this.owner = null; + this.ImFont = default; + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index b038d44ba..ce5a09b22 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -761,7 +761,7 @@ public sealed class UiBuilder : IDisposable // Note: do not dispose w; we do not own it } - public IFontHandle.ImFontLocked Lock() => + public ILockedImFont Lock() => this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); public IDisposable Push() => @@ -775,7 +775,7 @@ public sealed class UiBuilder : IDisposable public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped})"; - private void WrappedOnImFontChanged(IFontHandle obj, IFontHandle.ImFontLocked lockedFont) => + private void WrappedOnImFontChanged(IFontHandle obj, ILockedImFont lockedFont) => this.ImFontChanged?.Invoke(obj, lockedFont); } } From 9d6756fbca58f6069a26ea54065aea7af48ef181 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Jan 2024 12:11:07 +0000 Subject: [PATCH 60/71] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index e9341bb30..d0108d2e6 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e9341bb3038bf4200300f21be4a8629525d15596 +Subproject commit d0108d2e6e30a6b51b1124fc27ec3523c8ca3acb From 31c3c1ecc0c5306c481312d79312835c37460e66 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 28 Jan 2024 17:55:49 -0800 Subject: [PATCH 61/71] Fix reset and reload not working --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 4 ++-- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 5007691ab..18cac5cf2 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2556,7 +2556,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.MenuItem(Locs.PluginContext_DeletePluginConfigReload)) { - this.ShowDeletePluginConfigWarningModal(plugin.Name).ContinueWith(t => + this.ShowDeletePluginConfigWarningModal(plugin.Manifest.InternalName).ContinueWith(t => { var shouldDelete = t.Result; @@ -2571,7 +2571,7 @@ internal class PluginInstallerWindow : Window, IDisposable { this.installStatus = OperationStatus.Idle; - this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(plugin.Name)); + this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(plugin.Manifest.InternalName)); }); } }); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 8bfb38c34..6bdf73036 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1094,7 +1094,7 @@ internal partial class PluginManager : IDisposable, IServiceType { try { - this.PluginConfigs.Delete(plugin.Name); + this.PluginConfigs.Delete(plugin.Manifest.InternalName); break; } catch (IOException) From e065f3e988315ebc29c5c82a73130f9e979d4126 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 28 Jan 2024 18:26:22 -0800 Subject: [PATCH 62/71] Show Name in text prompt --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 18cac5cf2..40fc66221 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2556,7 +2556,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.MenuItem(Locs.PluginContext_DeletePluginConfigReload)) { - this.ShowDeletePluginConfigWarningModal(plugin.Manifest.InternalName).ContinueWith(t => + this.ShowDeletePluginConfigWarningModal(plugin.Manifest.Name).ContinueWith(t => { var shouldDelete = t.Result; From 0e724be4f8b3e778761e0c65d7d27560c43a1f73 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 28 Jan 2024 18:27:36 -0800 Subject: [PATCH 63/71] Fix loc typo --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 40fc66221..83d819634 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3773,7 +3773,7 @@ internal class PluginInstallerWindow : Window, IDisposable public static string DeletePluginConfigWarningModal_Title => Loc.Localize("InstallerDeletePluginConfigWarning", "Warning###InstallerDeletePluginConfigWarning"); - public static string DeletePluginConfigWarningModal_Body(string pluginName) => Loc.Localize("InstallerDeletePluginConfigWarningBody", "Are you sure you want to delete all data and configuration for v{0}?").Format(pluginName); + public static string DeletePluginConfigWarningModal_Body(string pluginName) => Loc.Localize("InstallerDeletePluginConfigWarningBody", "Are you sure you want to delete all data and configuration for {0}?").Format(pluginName); public static string DeletePluginConfirmWarningModal_Yes => Loc.Localize("InstallerDeletePluginConfigWarningYes", "Yes"); From 866c41c2d8282258ef9d56796113009053142898 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 30 Jan 2024 03:03:31 +0100 Subject: [PATCH 64/71] Update ClientStructs (#1623) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index d0108d2e6..0549ab9a9 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit d0108d2e6e30a6b51b1124fc27ec3523c8ca3acb +Subproject commit 0549ab9a993b4c4c8c0b4dcd4e31ed5413f75387 From 65265b678e1e56c28e4c23dc2f89f18b76c95a32 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 4 Feb 2024 19:57:00 +0100 Subject: [PATCH 65/71] Update ClientStructs (#1626) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 0549ab9a9..b5f5f68e1 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 0549ab9a993b4c4c8c0b4dcd4e31ed5413f75387 +Subproject commit b5f5f68e147e1a21a0f0c88345f8d8c359678317 From 1d32e8fe45821de870f8730f58d9e3e60a12885d Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 7 Feb 2024 18:33:35 +0000 Subject: [PATCH 66/71] Fix language selector throwing a exception, use native name for Taiwan (#1634) The language selector has only been showing language codes and not the actual language names since dd0159ae5a2174819c1541644e5cdbd4ddd98a1d because "tw" (Taiwan Mandarin) was added and it's not supported by CultureInfo. This adds a specific check like the language code to work around this and stop throwing exceptions. Also converts to a switch so it looks a bit nicer. --- .../Widgets/LanguageChooserSettingsEntry.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs index 85f8a826f..c8cc1f42c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs @@ -31,17 +31,20 @@ public sealed class LanguageChooserSettingsEntry : SettingsEntry try { var locLanguagesList = new List(); - string locLanguage; foreach (var language in this.languages) { - if (language != "ko") + switch (language) { - locLanguage = CultureInfo.GetCultureInfo(language).NativeName; - locLanguagesList.Add(char.ToUpper(locLanguage[0]) + locLanguage[1..]); - } - else - { - locLanguagesList.Add("Korean"); + case "ko": + locLanguagesList.Add("Korean"); + break; + case "tw": + locLanguagesList.Add("中華民國國語"); + break; + default: + string locLanguage = CultureInfo.GetCultureInfo(language).NativeName; + locLanguagesList.Add(char.ToUpper(locLanguage[0]) + locLanguage[1..]); + break; } } From 7112651b7762ac1f5554811e7a820e97a7fbb779 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 7 Feb 2024 18:37:55 +0000 Subject: [PATCH 67/71] Remove EnableWindowsTargeting from build.sh's run step (#1633) This removes the property that shouldn't be there, because it was considered a target. --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 build.sh diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index a4c346c80..5aa50b1c1 --- a/build.sh +++ b/build.sh @@ -59,4 +59,4 @@ fi echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false /p:EnableWindowsTargeting=true -nologo -clp:NoSummary --verbosity quiet -"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- /p:EnableWindowsTargeting=true "$@" +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" From 8b30781b4c2b4f5169d254bd4a17f4c040a5e0dd Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 7 Feb 2024 13:06:03 -0500 Subject: [PATCH 68/71] Change "Enable AntiDebug" label to make it clearer You need to enable this to allow debugging, but the label has the negative which doesn't make sense. Now it's called "Disable Debugging Protections" which is what it actually does. --- Dalamud/Interface/Internal/DalamudInterface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 60c1f9957..6035ca0ec 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -667,7 +667,7 @@ internal class DalamudInterface : IDisposable, IServiceType } var antiDebug = Service.Get(); - if (ImGui.MenuItem("Enable AntiDebug", null, antiDebug.IsEnabled)) + if (ImGui.MenuItem("Disable Debugging Protections", null, antiDebug.IsEnabled)) { var newEnabled = !antiDebug.IsEnabled; if (newEnabled) From 0d10a179664af93c428dd5fabdf38a0be8dbc306 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 8 Feb 2024 09:53:51 -0800 Subject: [PATCH 69/71] Make CommandWidget look better (and expose more info) (#1631) - Expose the plugin that owns the command. --- .../Windows/Data/Widgets/CommandWidget.cs | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs index 8ec704888..c4c74274a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs @@ -1,4 +1,8 @@ -using Dalamud.Game.Command; +using System.Linq; + +using Dalamud.Game.Command; +using Dalamud.Interface.Utility.Raii; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -28,9 +32,52 @@ internal class CommandWidget : IDataWindowWidget { var commandManager = Service.Get(); - foreach (var command in commandManager.Commands) + var tableFlags = ImGuiTableFlags.ScrollY | ImGuiTableFlags.Borders | ImGuiTableFlags.SizingStretchProp | + ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate; + using var table = ImRaii.Table("CommandList", 4, tableFlags); + if (table) { - ImGui.Text($"{command.Key}\n -> {command.Value.HelpMessage}\n -> In help: {command.Value.ShowInHelp}\n\n"); + ImGui.TableSetupScrollFreeze(0, 1); + + ImGui.TableSetupColumn("Command"); + ImGui.TableSetupColumn("Plugin"); + ImGui.TableSetupColumn("HelpMessage", ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupColumn("In Help?", ImGuiTableColumnFlags.NoSort); + ImGui.TableHeadersRow(); + + var sortSpecs = ImGui.TableGetSortSpecs(); + var commands = commandManager.Commands.ToArray(); + + if (sortSpecs.SpecsCount != 0) + { + commands = sortSpecs.Specs.ColumnIndex switch + { + 0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? commands.OrderBy(kv => kv.Key).ToArray() + : commands.OrderByDescending(kv => kv.Key).ToArray(), + 1 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? commands.OrderBy(kv => kv.Value.LoaderAssemblyName).ToArray() + : commands.OrderByDescending(kv => kv.Value.LoaderAssemblyName).ToArray(), + _ => commands, + }; + } + + foreach (var command in commands) + { + ImGui.TableNextRow(); + + ImGui.TableSetColumnIndex(0); + ImGui.Text(command.Key); + + ImGui.TableNextColumn(); + ImGui.Text(command.Value.LoaderAssemblyName); + + ImGui.TableNextColumn(); + ImGui.TextWrapped(command.Value.HelpMessage); + + ImGui.TableNextColumn(); + ImGui.Text(command.Value.ShowInHelp ? "Yes" : "No"); + } } } } From df65d59f8b376631337948cea2c4bd1746b2c904 Mon Sep 17 00:00:00 2001 From: marzent Date: Sat, 10 Feb 2024 13:03:11 +0100 Subject: [PATCH 70/71] add more exception handler options to dev menu --- Dalamud/Dalamud.cs | 52 +++++++++++++++---- .../Interface/Internal/DalamudInterface.cs | 14 ++++- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 4ab617d0a..8c858ce7c 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -117,6 +117,14 @@ internal sealed class Dalamud : IServiceType } }); } + + this.DefaultExceptionFilter = NativeFunctions.SetUnhandledExceptionFilter(nint.Zero); + NativeFunctions.SetUnhandledExceptionFilter(this.DefaultExceptionFilter); + Log.Debug($"SE default exception filter at {this.DefaultExceptionFilter.ToInt64():X}"); + + var debugSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??"; + this.DebugExceptionFilter = Service.Get().ScanText(debugSig); + Log.Debug($"SE debug exception filter at {this.DebugExceptionFilter.ToInt64():X}"); } /// @@ -128,7 +136,17 @@ internal sealed class Dalamud : IServiceType /// Gets location of stored assets. /// internal DirectoryInfo AssetDirectory => new(this.StartInfo.AssetDirectory!); - + + /// + /// Gets the in-game default exception filter. + /// + private nint DefaultExceptionFilter { get; } + + /// + /// Gets the in-game debug exception filter. + /// + private nint DebugExceptionFilter { get; } + /// /// Signal to the crash handler process that we should restart the game. /// @@ -191,18 +209,32 @@ internal sealed class Dalamud : IServiceType } /// - /// Replace the built-in exception handler with a debug one. + /// Replace the current exception handler with the default one. /// - internal void ReplaceExceptionHandler() - { - var releaseSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??"; - var releaseFilter = Service.Get().ScanText(releaseSig); - Log.Debug($"SE debug filter at {releaseFilter.ToInt64():X}"); + internal void UseDefaultExceptionHandler() => + this.SetExceptionHandler(this.DefaultExceptionFilter); - var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter); - Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter); + /// + /// Replace the current exception handler with a debug one. + /// + internal void UseDebugExceptionHandler() => + this.SetExceptionHandler(this.DebugExceptionFilter); + + /// + /// Disable the current exception handler. + /// + internal void UseNoExceptionHandler() => + this.SetExceptionHandler(nint.Zero); + + /// + /// Helper function to set the exception handler. + /// + private void SetExceptionHandler(nint newFilter) + { + var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(newFilter); + Log.Debug("Set ExceptionFilter to {0}, old: {1}", newFilter, oldFilter); } - + private void SetupClientStructsResolver(DirectoryInfo cacheDir) { using (Timings.Start("CS Resolver Init")) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 6035ca0ec..b8ca98584 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -863,9 +863,19 @@ internal class DalamudInterface : IDisposable, IServiceType if (ImGui.BeginMenu("Game")) { - if (ImGui.MenuItem("Replace ExceptionHandler")) + if (ImGui.MenuItem("Use in-game default ExceptionHandler")) { - this.dalamud.ReplaceExceptionHandler(); + this.dalamud.UseDefaultExceptionHandler(); + } + + if (ImGui.MenuItem("Use in-game debug ExceptionHandler")) + { + this.dalamud.UseDebugExceptionHandler(); + } + + if (ImGui.MenuItem("Disable in-game ExceptionHandler")) + { + this.dalamud.UseNoExceptionHandler(); } ImGui.EndMenu(); From 16bc6b86e5fed795539d3929b732c3f292057254 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 11 Feb 2024 19:20:26 +0100 Subject: [PATCH 71/71] Update ClientStructs (#1630) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index b5f5f68e1..e3bd59106 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit b5f5f68e147e1a21a0f0c88345f8d8c359678317 +Subproject commit e3bd5910678683a718e68f0f940c88b08c24eba5