diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml index 0b585d7ed..c885dd0f9 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml @@ -1,18 +1,21 @@ - + @@ -21,64 +24,67 @@ - + - - + + + HorizontalAlignment="Stretch" + Orientation="Vertical" + Spacing="4"> + Text="{Binding SearchText, Mode=TwoWay}" + Watermark="{x:Static lang:Resources.Label_SearchEllipsis}"> - + - + - + - + - + @@ -87,13 +93,13 @@ @@ -101,18 +107,18 @@ - + - + @@ -120,13 +126,13 @@ @@ -134,26 +140,37 @@ - + - + - + + + + + + + + + + + - @@ -168,5 +185,5 @@ - + diff --git a/StabilityMatrix.Core/Models/LaunchOption.cs b/StabilityMatrix.Core/Models/LaunchOption.cs index 16d3b41c0..9c24d25f2 100644 --- a/StabilityMatrix.Core/Models/LaunchOption.cs +++ b/StabilityMatrix.Core/Models/LaunchOption.cs @@ -9,13 +9,20 @@ public class LaunchOption public required string Name { get; init; } public LaunchOptionType Type { get; init; } = LaunchOptionType.Bool; - + [JsonIgnore] public object? DefaultValue { get; init; } - + [JsonIgnore] public bool HasDefaultValue => DefaultValue != null; + /// + /// When set, this option is exclusive with other options sharing the same GroupName + /// (rendered as radio buttons instead of checkboxes). + /// + [JsonIgnore] + public string? GroupName { get; init; } + [JsonConverter(typeof(LaunchOptionValueJsonConverter))] public object? OptionValue { get; set; } @@ -29,11 +36,9 @@ public bool IsEmptyOrDefault() return Type switch { LaunchOptionType.Bool => OptionValue == null, - LaunchOptionType.Int => OptionValue == null || - (int?) OptionValue == (int?) DefaultValue, - LaunchOptionType.String => OptionValue == null || - (string?) OptionValue == (string?) DefaultValue, - _ => throw new ArgumentOutOfRangeException() + LaunchOptionType.Int => OptionValue == null || (int?)OptionValue == (int?)DefaultValue, + LaunchOptionType.String => OptionValue == null || (string?)OptionValue == (string?)DefaultValue, + _ => throw new ArgumentOutOfRangeException(), }; } @@ -48,7 +53,7 @@ public bool IsEmptyOrDefault() LaunchOptionType.Bool => bool.TryParse(value, out var boolValue) ? boolValue : null, LaunchOptionType.Int => int.TryParse(value, out var intValue) ? intValue : null, LaunchOptionType.String => value, - _ => throw new ArgumentException($"Unknown option type {type}") + _ => throw new ArgumentException($"Unknown option type {type}"), }; } @@ -63,17 +68,19 @@ public bool IsEmptyOrDefault() switch (Type) { case LaunchOptionType.Bool: - return (bool?) OptionValue == true ? Name : null; + return (bool?)OptionValue == true ? Name : null; case LaunchOptionType.Int: - return (int?) OptionValue != null ? $"{Name} {OptionValue}" : null; + return (int?)OptionValue != null ? $"{Name} {OptionValue}" : null; case LaunchOptionType.String: - var valueString = (string?) OptionValue; + var valueString = (string?)OptionValue; // Special case empty string name to not do quoting (for custom launch args) if (Name == "") { return valueString; } - return string.IsNullOrWhiteSpace(valueString) ? null : $"{Name} {ProcessRunner.Quote(valueString)}"; + return string.IsNullOrWhiteSpace(valueString) + ? null + : $"{Name} {ProcessRunner.Quote(valueString)}"; default: throw new ArgumentOutOfRangeException(); } diff --git a/StabilityMatrix.Core/Models/LaunchOptionCard.cs b/StabilityMatrix.Core/Models/LaunchOptionCard.cs index 8882029cf..a0a988b6b 100644 --- a/StabilityMatrix.Core/Models/LaunchOptionCard.cs +++ b/StabilityMatrix.Core/Models/LaunchOptionCard.cs @@ -9,7 +9,13 @@ public readonly record struct LaunchOptionCard public required LaunchOptionType Type { get; init; } public required IReadOnlyList Options { get; init; } public string? Description { get; init; } - + public int? MaxSelectedOptions { get; init; } + + /// + /// True if this card's options are mutually exclusive and should be rendered as radio buttons. + /// + public bool IsSingleSelect => MaxSelectedOptions == 1; + public static LaunchOptionCard FromDefinition(LaunchOptionDefinition definition) { return new LaunchOptionCard @@ -17,20 +23,24 @@ public static LaunchOptionCard FromDefinition(LaunchOptionDefinition definition) Title = definition.Name, Description = definition.Description, Type = definition.Type, - - Options = definition.Options.Select(s => - { - var option = new LaunchOption + MaxSelectedOptions = definition.MaxSelectedOptions, + + Options = definition + .Options.Select(s => { - Name = s, - Type = definition.Type, - DefaultValue = definition.DefaultValue - }; - return option; - }).ToImmutableArray() + var option = new LaunchOption + { + Name = s, + Type = definition.Type, + DefaultValue = definition.DefaultValue, + GroupName = definition.MaxSelectedOptions == 1 ? definition.Name : null, + }; + return option; + }) + .ToImmutableArray(), }; } - + /// /// Yield LaunchOptionCards given definitions and launch args to load /// @@ -39,20 +49,18 @@ public static LaunchOptionCard FromDefinition(LaunchOptionDefinition definition) /// /// public static IEnumerable FromDefinitions( - IEnumerable definitions, - IEnumerable launchArgs) + IEnumerable definitions, + IEnumerable launchArgs + ) { // During card creation, store dict of options with initial values var initialOptions = new Dictionary(); - + // To dictionary ignoring duplicates var launchArgsDict = launchArgs .ToLookup(launchArg => launchArg.Name) - .ToDictionary( - group => group.Key, - group => group.First() - ); - + .ToDictionary(group => group.Key, group => group.First()); + // Create cards foreach (var definition in definitions) { @@ -60,8 +68,9 @@ public static IEnumerable FromDefinitions( if (definition.Type != LaunchOptionType.Bool && definition.Options.Count != 1) { throw new InvalidOperationException( - $"Definition: '{definition.Name}' has {definition.Options.Count} options," + - $" it must have exactly 1 option for non-bool types"); + $"Definition: '{definition.Name}' has {definition.Options.Count} options," + + $" it must have exactly 1 option for non-bool types" + ); } // Store initial values if (definition.InitialValue != null) @@ -77,12 +86,15 @@ public static IEnumerable FromDefinitions( else { // For single/multiple options (string only) - var option = definition.Options.FirstOrDefault(opt => opt.Equals(definition.InitialValue)); + var option = definition.Options.FirstOrDefault(opt => + opt.Equals(definition.InitialValue) + ); if (option == null) { throw new InvalidOperationException( - $"Definition '{definition.Name}' has InitialValue of '{definition.InitialValue}', but it was not found in options:" + - $" '{string.Join(",", definition.Options)}'"); + $"Definition '{definition.Name}' has InitialValue of '{definition.InitialValue}', but it was not found in options:" + + $" '{string.Join(",", definition.Options)}'" + ); } initialOptions[option] = true; } @@ -99,31 +111,37 @@ public static IEnumerable FromDefinitions( Title = definition.Name, Description = definition.Description, Type = definition.Type, - Options = definition.Options.Select(s => - { - // Parse defaults and user loaded values here - var userOption = launchArgsDict.GetValueOrDefault(s); - var userValue = userOption?.OptionValue; - // If no user value, check set initial value - if (userValue is null) - { - var initialValue = initialOptions.GetValueOrDefault(s); - userValue ??= initialValue; - Debug.WriteLineIf(initialValue != null, - $"Using initial value {initialValue} for option {s}"); - } - - var option = new LaunchOption + MaxSelectedOptions = definition.MaxSelectedOptions, + Options = definition + .Options.Select(s => { - Name = s, - Type = definition.Type, - DefaultValue = definition.DefaultValue, - OptionValue = userValue - }; - return option; - }).ToImmutableArray() + // Parse defaults and user loaded values here + var userOption = launchArgsDict.GetValueOrDefault(s); + var userValue = userOption?.OptionValue; + // If no user value, check set initial value + if (userValue is null) + { + var initialValue = initialOptions.GetValueOrDefault(s); + userValue ??= initialValue; + Debug.WriteLineIf( + initialValue != null, + $"Using initial value {initialValue} for option {s}" + ); + } + + var option = new LaunchOption + { + Name = s, + Type = definition.Type, + DefaultValue = definition.DefaultValue, + OptionValue = userValue, + GroupName = definition.MaxSelectedOptions == 1 ? definition.Name : null, + }; + return option; + }) + .ToImmutableArray(), }; - + yield return card; } } diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index ec9b594a0..dc6c2cdc0 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -334,12 +334,14 @@ private static DirectoryPath GetSharedWorkflowsLinkDir(DirectoryPath installDire InitialValue = DefaultToQuadCrossAttention() ? "--use-quad-cross-attention" // For Legacy AMD GPUs. : "--use-pytorch-cross-attention", + MaxSelectedOptions = 1, Options = [ "--use-split-cross-attention", "--use-quad-cross-attention", "--use-pytorch-cross-attention", "--use-sage-attention", + "--use-ck-attention", ], }, new() diff --git a/StabilityMatrix.Core/Models/Packages/ComfyZluda.cs b/StabilityMatrix.Core/Models/Packages/ComfyZluda.cs index 62f5a7996..9d29a9d89 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyZluda.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyZluda.cs @@ -67,12 +67,14 @@ public override List LaunchOptions Name = "Cross Attention Method", Type = LaunchOptionType.Bool, InitialValue = "--use-quad-cross-attention", + MaxSelectedOptions = 1, Options = [ "--use-split-cross-attention", "--use-quad-cross-attention", "--use-pytorch-cross-attention", "--use-sage-attention", + "--use-ck-attention", ], }, new()