From 81b72222ea27a4d3f064c2193b9313145ed400c1 Mon Sep 17 00:00:00 2001 From: KakaruHayate Date: Tue, 2 Jun 2026 20:08:28 +0800 Subject: [PATCH 1/2] Add prioritized render scheduling --- OpenUtau.Core/Commands/ExpCommands.cs | 56 ++++++++++- OpenUtau.Core/Commands/NoteCommands.cs | 11 +++ OpenUtau.Core/Commands/Notifications.cs | 32 +++++- OpenUtau.Core/Commands/PartCommands.cs | 8 ++ OpenUtau.Core/Commands/RenderInvalidation.cs | 17 ++++ OpenUtau.Core/Commands/UCommand.cs | 2 + OpenUtau.Core/DocManager.cs | 27 +++++- OpenUtau.Core/PlaybackManager.cs | 12 ++- OpenUtau.Core/Properties/AssemblyInfo.cs | 3 + OpenUtau.Core/Render/RenderEngine.cs | 97 ++++++++++++++++--- OpenUtau.Core/Render/RenderPriority.cs | 52 ++++++++++ .../Core/Commands/RenderInvalidationTest.cs | 55 +++++++++++ .../Core/Render/RenderPriorityTest.cs | 38 ++++++++ 13 files changed, 389 insertions(+), 21 deletions(-) create mode 100644 OpenUtau.Core/Commands/RenderInvalidation.cs create mode 100644 OpenUtau.Core/Properties/AssemblyInfo.cs create mode 100644 OpenUtau.Core/Render/RenderPriority.cs create mode 100644 OpenUtau.Test/Core/Commands/RenderInvalidationTest.cs create mode 100644 OpenUtau.Test/Core/Render/RenderPriorityTest.cs diff --git a/OpenUtau.Core/Commands/ExpCommands.cs b/OpenUtau.Core/Commands/ExpCommands.cs index 072e12c71..d43f3bc85 100644 --- a/OpenUtau.Core/Commands/ExpCommands.cs +++ b/OpenUtau.Core/Commands/ExpCommands.cs @@ -18,6 +18,14 @@ public override ValidateOptions ValidateOptions public ExpCommand(UVoicePart part) { Part = part; } + public override IEnumerable GetRenderInvalidations() { + if (Note != null) { + return new[] { + new RenderInvalidation(Part, Part.position + Note.position, Part.position + Note.End) + }; + } + return new[] { new RenderInvalidation(Part, Part.position, Part.End) }; + } } public class SetNoteExpressionCommand : ExpCommand { @@ -83,6 +91,17 @@ public override void Unexecute() { notes[i].SetExpression(project, track, Key, oldValue[i]); } } + public override IEnumerable GetRenderInvalidations() { + if (notes.Length == 0) { + return Enumerable.Empty(); + } + return new[] { + new RenderInvalidation( + Part, + Part.position + notes.Min(note => note.position), + Part.position + notes.Max(note => note.End)) + }; + } } public class SetPhonemeExpressionCommand : ExpCommand { @@ -317,6 +336,8 @@ public class SetCurveCommand : ExpCommand { readonly int lastY; int[] oldXs; int[] oldYs; + int StartTick => Math.Min(x, lastX); + int EndTick => Math.Max(x, lastX) + 1; public override ValidateOptions ValidateOptions => new ValidateOptions { SkipTiming = true, @@ -366,14 +387,19 @@ public override bool CanMerge(IList commands) { public override UCommand Merge(IList commands) { var first = commands.First() as SetCurveCommand; var last = commands.Last() as SetCurveCommand; + var curveCommands = commands.Cast().ToArray(); var curve = Part.curves.FirstOrDefault(c => c.abbr == abbr); curve.Simplify(); int[] newXs = curve?.xs.ToArray(); int[] newYs = curve?.ys.ToArray(); return new MergedSetCurveCommand( last.project, last.Part, last.abbr, - first.oldXs, first.oldYs, newXs, newYs); + first.oldXs, first.oldYs, newXs, newYs, + startTick: curveCommands.Min(command => command.StartTick), + endTick: curveCommands.Max(command => command.EndTick)); } + public override IEnumerable GetRenderInvalidations() => + new[] { new RenderInvalidation(Part, Part.position + StartTick, Part.position + EndTick) }; } public class MergedSetCurveCommand : ExpCommand { @@ -384,8 +410,11 @@ public class MergedSetCurveCommand : ExpCommand { readonly int[] newXs; readonly int[] newYs; readonly bool setReal; + readonly int startTick; + readonly int endTick; public MergedSetCurveCommand(UProject project, UVoicePart part, - string abbr, int[] oldXs, int[] oldYs, int[] newXs, int[] newYs, bool setReal = false) : base(part) { + string abbr, int[] oldXs, int[] oldYs, int[] newXs, int[] newYs, bool setReal = false, + int startTick = -1, int endTick = -1) : base(part) { this.project = project; this.abbr = abbr; this.oldXs = oldXs; @@ -393,6 +422,8 @@ public MergedSetCurveCommand(UProject project, UVoicePart part, this.newXs = newXs; this.newYs = newYs; this.setReal = setReal; + this.startTick = startTick >= 0 ? startTick : MergedStartTick(oldXs, newXs); + this.endTick = endTick >= 0 ? endTick : MergedEndTick(oldXs, newXs); } public override string ToString() => "Edit Curve"; public override void Execute() { @@ -427,6 +458,20 @@ public override void Unexecute() { private List? GetCurveYs(UCurve? curve) { return setReal ? curve?.realYs : curve?.ys; } + static int MergedStartTick(int[]? oldXs, int[]? newXs) { + return (oldXs ?? Array.Empty()) + .Concat(newXs ?? Array.Empty()) + .DefaultIfEmpty(0) + .Min(); + } + int MergedEndTick(int[]? oldXs, int[]? newXs) { + return (oldXs ?? Array.Empty()) + .Concat(newXs ?? Array.Empty()) + .DefaultIfEmpty(Part.Duration) + .Max() + 1; + } + public override IEnumerable GetRenderInvalidations() => + new[] { new RenderInvalidation(Part, Part.position + startTick, Part.position + endTick) }; } public class PasteCurveCommand : ExpCommand { @@ -491,6 +536,11 @@ public override void Unexecute() { curve.ys.AddRange(oldYs); } } + public override IEnumerable GetRenderInvalidations() { + int startTick = xs.DefaultIfEmpty(0).Min(); + int endTick = xs.DefaultIfEmpty(Part.Duration).Max() + 1; + return new[] { new RenderInvalidation(Part, Part.position + startTick, Part.position + endTick) }; + } } public class ClearCurveCommand : ExpCommand { @@ -522,5 +572,7 @@ public override void Unexecute() { curve.ys.AddRange(oldYs); } } + public override IEnumerable GetRenderInvalidations() => + new[] { new RenderInvalidation(Part, Part.position, Part.End) }; } } diff --git a/OpenUtau.Core/Commands/NoteCommands.cs b/OpenUtau.Core/Commands/NoteCommands.cs index 85bde011f..e3c1fbe4d 100644 --- a/OpenUtau.Core/Commands/NoteCommands.cs +++ b/OpenUtau.Core/Commands/NoteCommands.cs @@ -19,6 +19,17 @@ public NoteCommand(UVoicePart part, IEnumerable notes) { Part = part; Notes = notes.ToArray(); } + public override IEnumerable GetRenderInvalidations() { + if (Notes.Length == 0) { + return Enumerable.Empty(); + } + return new[] { + new RenderInvalidation( + Part, + Part.position + Notes.Min(note => note.position), + Part.position + Notes.Max(note => note.End)) + }; + } } public class AddNoteCommand : NoteCommand { diff --git a/OpenUtau.Core/Commands/Notifications.cs b/OpenUtau.Core/Commands/Notifications.cs index 0ee6448fa..d2f91e2a4 100644 --- a/OpenUtau.Core/Commands/Notifications.cs +++ b/OpenUtau.Core/Commands/Notifications.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using OpenUtau.Core.Ustx; namespace OpenUtau.Core { @@ -222,8 +224,36 @@ public FocusNoteNotification(UPart part, UNote note) { public override string ToString() => $"Focus note {note.lyric} at {note.position}."; } + public class PreRenderPriority { + public readonly UVoicePart part; + public readonly int startTick; + public readonly int endTick; + + public PreRenderPriority(UVoicePart part, int startTick, int endTick) { + this.part = part; + this.startTick = startTick; + this.endTick = endTick; + } + } + public class PreRenderNotification : UNotification { - public override string ToString() => $"Pre-render notification."; + public readonly PreRenderPriority[] priorities; + public bool HasPriorityRange => priorities.Length > 0; + + public PreRenderNotification() { + priorities = Array.Empty(); + } + + public PreRenderNotification(IEnumerable priorities) { + this.priorities = priorities + .Where(priority => priority.endTick > priority.startTick) + .ToArray(); + part = this.priorities.FirstOrDefault()?.part; + } + + public override string ToString() => HasPriorityRange + ? $"Pre-render notification {priorities.Length} prioritized range(s)." + : $"Pre-render notification."; } public class PartRenderedNotification : UNotification { diff --git a/OpenUtau.Core/Commands/PartCommands.cs b/OpenUtau.Core/Commands/PartCommands.cs index 43debf786..fe6ff0b19 100644 --- a/OpenUtau.Core/Commands/PartCommands.cs +++ b/OpenUtau.Core/Commands/PartCommands.cs @@ -1,5 +1,7 @@ using OpenUtau.Core.Ustx; using SharpCompress; +using System.Collections.Generic; +using System.Linq; namespace OpenUtau.Core { public abstract class PartCommand : UCommand { @@ -12,6 +14,12 @@ public PartCommand(UProject project, UPart part) { this.project = project; this.part = part; } + public override IEnumerable GetRenderInvalidations() { + if (part is UVoicePart voicePart) { + return new[] { new RenderInvalidation(voicePart, voicePart.position, voicePart.End) }; + } + return Enumerable.Empty(); + } } public class AddPartCommand : PartCommand { diff --git a/OpenUtau.Core/Commands/RenderInvalidation.cs b/OpenUtau.Core/Commands/RenderInvalidation.cs new file mode 100644 index 000000000..38427ee9a --- /dev/null +++ b/OpenUtau.Core/Commands/RenderInvalidation.cs @@ -0,0 +1,17 @@ +using OpenUtau.Core.Ustx; + +namespace OpenUtau.Core { + public readonly struct RenderInvalidation { + public readonly UVoicePart part; + public readonly int startTick; + public readonly int endTick; + + public RenderInvalidation(UVoicePart part, int startTick, int endTick) { + this.part = part; + this.startTick = startTick; + this.endTick = endTick; + } + + public bool IsValid => part != null && endTick > startTick; + } +} diff --git a/OpenUtau.Core/Commands/UCommand.cs b/OpenUtau.Core/Commands/UCommand.cs index a73eaf489..255eacde9 100644 --- a/OpenUtau.Core/Commands/UCommand.cs +++ b/OpenUtau.Core/Commands/UCommand.cs @@ -10,6 +10,8 @@ public abstract class UCommand { public abstract void Unexecute(); public virtual bool CanMerge(IList commands) => false; public virtual UCommand Merge(IList commands) => throw new NotImplementedException(); + public virtual IEnumerable GetRenderInvalidations() => + Enumerable.Empty(); public abstract override string ToString(); } diff --git a/OpenUtau.Core/DocManager.cs b/OpenUtau.Core/DocManager.cs index 22fb2fbba..5fa485ba9 100644 --- a/OpenUtau.Core/DocManager.cs +++ b/OpenUtau.Core/DocManager.cs @@ -278,10 +278,31 @@ public void EndUndoGroup() { if (undoGroup.DeferValidate) { Project.ValidateFull(); } + var preRenderNotification = CreatePreRenderNotification(undoGroup.Commands); undoGroup.Merge(); undoGroup = null; Log.Information("undoGroup ended"); - ExecuteCmd(new PreRenderNotification()); + ExecuteCmd(preRenderNotification); + } + + PreRenderNotification CreatePreRenderNotification(IEnumerable commands) { + var priorityRanges = new Dictionary(); + foreach (var invalidation in commands.SelectMany(command => command.GetRenderInvalidations())) { + if (!invalidation.IsValid || !Project.parts.Contains(invalidation.part)) { + continue; + } + if (priorityRanges.TryGetValue(invalidation.part, out var range)) { + priorityRanges[invalidation.part] = ( + Math.Min(range.startTick, invalidation.startTick), + Math.Max(range.endTick, invalidation.endTick)); + } else { + priorityRanges[invalidation.part] = (invalidation.startTick, invalidation.endTick); + } + } + return priorityRanges.Count > 0 + ? new PreRenderNotification(priorityRanges.Select(range => + new PreRenderPriority(range.Key, range.Value.startTick, range.Value.endTick))) + : new PreRenderNotification(); } public void RollBackUndoGroup() { @@ -314,7 +335,7 @@ public void Undo() { Publish(cmd, true); } redoQueue.AddToBack(group); - ExecuteCmd(new PreRenderNotification()); + ExecuteCmd(CreatePreRenderNotification(group.Commands)); } public void Redo() { @@ -331,7 +352,7 @@ public void Redo() { Publish(cmd); } undoQueue.AddToBack(group); - ExecuteCmd(new PreRenderNotification()); + ExecuteCmd(CreatePreRenderNotification(group.Commands)); } public bool GetUndoState(out string? key) { diff --git a/OpenUtau.Core/PlaybackManager.cs b/OpenUtau.Core/PlaybackManager.cs index 9bc67a6ed..fc1367bef 100644 --- a/OpenUtau.Core/PlaybackManager.cs +++ b/OpenUtau.Core/PlaybackManager.cs @@ -373,9 +373,13 @@ private void CheckFileWritable(string filePath) { } } - void SchedulePreRender() { - Log.Information("SchedulePreRender"); - var engine = new RenderEngine(DocManager.Inst.Project); + void SchedulePreRender(PreRenderNotification? notification = null) { + Log.Information(notification?.HasPriorityRange == true + ? $"SchedulePreRender {notification.priorities.Length} prioritized range(s)" + : "SchedulePreRender"); + var engine = notification?.HasPriorityRange == true + ? new RenderEngine(DocManager.Inst.Project, priorityRanges: notification.priorities) + : new RenderEngine(DocManager.Inst.Project); engine.PreRenderProject(ref renderCancellation); } @@ -404,7 +408,7 @@ public void OnNext(UCommand cmd, bool isUndo) { } if (cmd is PreRenderNotification || cmd is LoadProjectNotification) { if (Util.Preferences.Default.PreRender) { - SchedulePreRender(); + SchedulePreRender(cmd as PreRenderNotification); } } } diff --git a/OpenUtau.Core/Properties/AssemblyInfo.cs b/OpenUtau.Core/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..aa105cb38 --- /dev/null +++ b/OpenUtau.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("OpenUtau.Test")] diff --git a/OpenUtau.Core/Render/RenderEngine.cs b/OpenUtau.Core/Render/RenderEngine.cs index b8bde5fa8..140ddda25 100644 --- a/OpenUtau.Core/Render/RenderEngine.cs +++ b/OpenUtau.Core/Render/RenderEngine.cs @@ -47,12 +47,21 @@ class RenderEngine { readonly int startTick; readonly int endTick; readonly int trackNo; + readonly PreRenderPriority[] priorityRanges; - public RenderEngine(UProject project, int startTick = 0, int endTick = -1, int trackNo = -1) { + public RenderEngine( + UProject project, + int startTick = 0, + int endTick = -1, + int trackNo = -1, + IEnumerable? priorityRanges = null) { this.project = project; this.startTick = startTick; this.endTick = endTick; this.trackNo = trackNo; + this.priorityRanges = priorityRanges? + .Where(priority => priority.endTick > priority.startTick) + .ToArray() ?? Array.Empty(); } // for playback or export @@ -220,21 +229,24 @@ private void RenderRequests( } var tuples = requests .SelectMany(req => req.phrases - .Zip(req.sources, (phrase, source) => Tuple.Create(phrase, source, req))) + .Zip(req.sources, (phrase, source) => (phrase, source, request: req))) .ToArray(); + if (tuples.Length == 0) { + return; + } if (playing) { - var orderedTuples = tuples - .Where(tuple => tuple.Item1.end > startTick) - .OrderBy(tuple => tuple.Item1.end) - .Concat(tuples.Where(tuple => tuple.Item1.end <= startTick)) - .ToArray(); - tuples = orderedTuples; + tuples = OrderForPlayback(tuples); + } else if (priorityRanges.Length > 0) { + tuples = OrderForPreRender(tuples); } var progress = new Progress(tuples.Sum(t => t.Item1.phones.Length)); foreach (var tuple in tuples) { - var phrase = tuple.Item1; - var source = tuple.Item2; - var request = tuple.Item3; + if (cancellation.IsCancellationRequested) { + break; + } + var phrase = tuple.phrase; + var source = tuple.source; + var request = tuple.request; var task = phrase.renderer.Render(phrase, progress, request.trackNo, cancellation, true); task.Wait(); if (cancellation.IsCancellationRequested) { @@ -249,6 +261,69 @@ private void RenderRequests( progress.Clear(); } + private (RenderPhrase phrase, WaveSource source, RenderPartRequest request)[] OrderForPlayback( + (RenderPhrase phrase, WaveSource source, RenderPartRequest request)[] tuples) { + double playbackStartMs = project.timeAxis.TickPosToMsPos(startTick); + return tuples + .Select((tuple, index) => (tuple, index)) + .OrderBy(item => RenderPriority.PlaybackBucket( + item.tuple.source.offsetMs, item.tuple.source.EndMs, playbackStartMs)) + .ThenBy(item => RenderPriority.PlaybackDistance( + item.tuple.source.offsetMs, item.tuple.source.EndMs, playbackStartMs)) + .ThenBy(item => item.index) + .Select(item => item.tuple) + .ToArray(); + } + + private (RenderPhrase phrase, WaveSource source, RenderPartRequest request)[] OrderForPreRender( + (RenderPhrase phrase, WaveSource source, RenderPartRequest request)[] tuples) { + return tuples + .Select((tuple, index) => (tuple, index)) + .OrderBy(item => PreRenderPriorityBucket(item.tuple)) + .ThenBy(item => PreRenderPriorityIndex(item.tuple)) + .ThenBy(item => PreRenderPriorityDistance(item.tuple.phrase)) + .ThenBy(item => item.index) + .Select(item => item.tuple) + .ToArray(); + } + + private int PreRenderPriorityBucket( + (RenderPhrase phrase, WaveSource source, RenderPartRequest request) tuple) { + bool isPriorityPart = priorityRanges.Any(priority => ReferenceEquals(tuple.request.part, priority.part)); + bool overlapsPriority = priorityRanges.Any(priority => + ReferenceEquals(tuple.request.part, priority.part) && + RenderPriority.Overlaps(tuple.phrase.position, tuple.phrase.end, priority.startTick, priority.endTick)); + int earliestPriorityStart = priorityRanges.Min(priority => priority.startTick); + return RenderPriority.PreRenderBucket( + isPriorityPart, + overlapsPriority, + tuple.phrase.end > earliestPriorityStart); + } + + private int PreRenderPriorityIndex( + (RenderPhrase phrase, WaveSource source, RenderPartRequest request) tuple) { + for (int i = 0; i < priorityRanges.Length; ++i) { + var priority = priorityRanges[i]; + if (ReferenceEquals(tuple.request.part, priority.part) && + RenderPriority.Overlaps(tuple.phrase.position, tuple.phrase.end, priority.startTick, priority.endTick)) { + return i; + } + } + for (int i = 0; i < priorityRanges.Length; ++i) { + if (ReferenceEquals(tuple.request.part, priorityRanges[i].part)) { + return i; + } + } + return int.MaxValue; + } + + private int PreRenderPriorityDistance(RenderPhrase phrase) { + return priorityRanges + .Select(priority => RenderPriority.PreRenderDistance(phrase.position, phrase.end, priority.startTick)) + .DefaultIfEmpty(0) + .Min(); + } + public static void ReleaseSourceTemp() { VoicebankFiles.Inst.ReleaseSourceTemp(); } diff --git a/OpenUtau.Core/Render/RenderPriority.cs b/OpenUtau.Core/Render/RenderPriority.cs new file mode 100644 index 000000000..54a2619bf --- /dev/null +++ b/OpenUtau.Core/Render/RenderPriority.cs @@ -0,0 +1,52 @@ +using System; + +namespace OpenUtau.Core.Render { + internal static class RenderPriority { + internal static int PlaybackBucket(double sourceStartMs, double sourceEndMs, double playbackStartMs) { + if (sourceStartMs <= playbackStartMs && sourceEndMs > playbackStartMs) { + return 0; + } + return sourceStartMs >= playbackStartMs ? 1 : 2; + } + + internal static double PlaybackDistance(double sourceStartMs, double sourceEndMs, double playbackStartMs) { + if (sourceStartMs <= playbackStartMs && sourceEndMs > playbackStartMs) { + return Math.Max(0, playbackStartMs - sourceStartMs); + } + if (sourceStartMs >= playbackStartMs) { + return sourceStartMs - playbackStartMs; + } + return playbackStartMs - sourceEndMs; + } + + internal static bool Overlaps(int startTick, int endTick, int priorityStartTick, int priorityEndTick) { + return endTick > priorityStartTick && startTick < priorityEndTick; + } + + internal static int PreRenderBucket( + bool isPriorityPart, + bool overlapsPriority, + bool isAfterPriorityStart) { + if (isPriorityPart && overlapsPriority) { + return 0; + } + if (isPriorityPart) { + return 1; + } + if (isAfterPriorityStart) { + return 2; + } + return 3; + } + + internal static int PreRenderDistance(int phraseStartTick, int phraseEndTick, int priorityStartTick) { + if (phraseStartTick <= priorityStartTick && phraseEndTick > priorityStartTick) { + return 0; + } + if (phraseStartTick >= priorityStartTick) { + return phraseStartTick - priorityStartTick; + } + return priorityStartTick - phraseEndTick; + } + } +} diff --git a/OpenUtau.Test/Core/Commands/RenderInvalidationTest.cs b/OpenUtau.Test/Core/Commands/RenderInvalidationTest.cs new file mode 100644 index 000000000..30d4aae1c --- /dev/null +++ b/OpenUtau.Test/Core/Commands/RenderInvalidationTest.cs @@ -0,0 +1,55 @@ +using System.Linq; +using OpenUtau.Core.Ustx; +using Xunit; + +namespace OpenUtau.Core { + public class RenderInvalidationTest { + [Fact] + public void NoteCommandReportsNoteRangeInProjectTicks() { + var part = new UVoicePart { position = 100, Duration = 1000 }; + var note = new UNote { position = 200, duration = 300 }; + var command = new AddNoteCommand(part, note); + + var invalidation = Assert.Single(command.GetRenderInvalidations()); + + Assert.Same(part, invalidation.part); + Assert.Equal(300, invalidation.startTick); + Assert.Equal(600, invalidation.endTick); + } + + [Fact] + public void SetCurveCommandReportsEditedRangeInProjectTicks() { + var project = new UProject(); + var part = new UVoicePart { position = 100, Duration = 1000 }; + var command = new SetCurveCommand( + project, part, Format.Ustx.PITD, x: 50, y: 0, lastX: 100, lastY: 0); + + var invalidation = Assert.Single(command.GetRenderInvalidations()); + + Assert.Same(part, invalidation.part); + Assert.Equal(150, invalidation.startTick); + Assert.Equal(201, invalidation.endTick); + } + + [Fact] + public void MergedSetCurveCommandKeepsMergedEditRange() { + var project = new UProject(); + var part = new UVoicePart { position = 100, Duration = 1000 }; + var command = new MergedSetCurveCommand( + project, + part, + Format.Ustx.PITD, + oldXs: new[] { 0, 400 }, + oldYs: new[] { 0, 0 }, + newXs: new[] { 0, 400 }, + newYs: new[] { 0, 0 }, + startTick: 50, + endTick: 101); + + var invalidation = Assert.Single(command.GetRenderInvalidations()); + + Assert.Equal(150, invalidation.startTick); + Assert.Equal(201, invalidation.endTick); + } + } +} diff --git a/OpenUtau.Test/Core/Render/RenderPriorityTest.cs b/OpenUtau.Test/Core/Render/RenderPriorityTest.cs new file mode 100644 index 000000000..19b9e7180 --- /dev/null +++ b/OpenUtau.Test/Core/Render/RenderPriorityTest.cs @@ -0,0 +1,38 @@ +using Xunit; + +namespace OpenUtau.Core.Render { + public class RenderPriorityTest { + [Fact] + public void PlaybackBucketPrioritizesCurrentThenFutureThenPast() { + Assert.Equal(0, RenderPriority.PlaybackBucket(100, 200, 150)); + Assert.Equal(1, RenderPriority.PlaybackBucket(200, 300, 150)); + Assert.Equal(2, RenderPriority.PlaybackBucket(0, 100, 150)); + } + + [Fact] + public void OverlapsUsesHalfOpenRanges() { + Assert.True(RenderPriority.Overlaps(10, 20, 19, 30)); + Assert.False(RenderPriority.Overlaps(10, 20, 20, 30)); + Assert.False(RenderPriority.Overlaps(10, 20, 0, 10)); + } + + [Fact] + public void PreRenderBucketPrioritizesOverlappingPriorityPart() { + Assert.Equal(0, RenderPriority.PreRenderBucket( + isPriorityPart: true, overlapsPriority: true, isAfterPriorityStart: true)); + Assert.Equal(1, RenderPriority.PreRenderBucket( + isPriorityPart: true, overlapsPriority: false, isAfterPriorityStart: true)); + Assert.Equal(2, RenderPriority.PreRenderBucket( + isPriorityPart: false, overlapsPriority: false, isAfterPriorityStart: true)); + Assert.Equal(3, RenderPriority.PreRenderBucket( + isPriorityPart: false, overlapsPriority: false, isAfterPriorityStart: false)); + } + + [Fact] + public void PreRenderDistancePrioritizesPhraseContainingPriorityStart() { + Assert.Equal(0, RenderPriority.PreRenderDistance(100, 200, 150)); + Assert.Equal(50, RenderPriority.PreRenderDistance(200, 300, 150)); + Assert.Equal(50, RenderPriority.PreRenderDistance(0, 100, 150)); + } + } +} From 08f923fcdc5919d8ed5d9d21de47b53ccf0afb66 Mon Sep 17 00:00:00 2001 From: KakaruHayate Date: Fri, 5 Jun 2026 00:06:14 +0800 Subject: [PATCH 2/2] Simplify render priority: remove GetRenderInvalidations() hook per maintainer review Based on stakira's feedback: - Remove GetRenderInvalidations() command hook (over-complicated, never used) - Remove RenderInvalidation.cs and RenderInvalidationTest.cs - Revert all command changes (NoteCommands, ExpCommands, PartCommands) - Revert DocManager, PlaybackManager, RenderEngine, Notifications changes Keep the existing RenderPriority and phrase ordering logic intact: - RenderPriority.cs (sorting rules) - OrderForPlayback / OrderForPreRender in RenderEngine - PreRenderNotification with focusPart/focusTick Update RenderPriorityTest: - Remove Overlaps tests (method doesn't exist in master) - Rename methods for consistency - Add PlaybackDistance coverage --- OpenUtau.Core/Commands/ExpCommands.cs | 56 +----------------- OpenUtau.Core/Commands/NoteCommands.cs | 11 ---- OpenUtau.Core/Commands/Notifications.cs | 33 ++--------- OpenUtau.Core/Commands/PartCommands.cs | 8 --- OpenUtau.Core/Commands/RenderInvalidation.cs | 17 ------ OpenUtau.Core/Commands/UCommand.cs | 2 - OpenUtau.Core/DocManager.cs | 27 +-------- OpenUtau.Core/PlaybackManager.cs | 34 ++++++++--- OpenUtau.Core/Render/RenderEngine.cs | 58 +++++++------------ OpenUtau.Core/Render/RenderPriority.cs | 4 -- .../Core/Commands/RenderInvalidationTest.cs | 55 ------------------ .../Core/Render/RenderPriorityTest.cs | 16 ++--- 12 files changed, 64 insertions(+), 257 deletions(-) delete mode 100644 OpenUtau.Core/Commands/RenderInvalidation.cs delete mode 100644 OpenUtau.Test/Core/Commands/RenderInvalidationTest.cs diff --git a/OpenUtau.Core/Commands/ExpCommands.cs b/OpenUtau.Core/Commands/ExpCommands.cs index d43f3bc85..072e12c71 100644 --- a/OpenUtau.Core/Commands/ExpCommands.cs +++ b/OpenUtau.Core/Commands/ExpCommands.cs @@ -18,14 +18,6 @@ public override ValidateOptions ValidateOptions public ExpCommand(UVoicePart part) { Part = part; } - public override IEnumerable GetRenderInvalidations() { - if (Note != null) { - return new[] { - new RenderInvalidation(Part, Part.position + Note.position, Part.position + Note.End) - }; - } - return new[] { new RenderInvalidation(Part, Part.position, Part.End) }; - } } public class SetNoteExpressionCommand : ExpCommand { @@ -91,17 +83,6 @@ public override void Unexecute() { notes[i].SetExpression(project, track, Key, oldValue[i]); } } - public override IEnumerable GetRenderInvalidations() { - if (notes.Length == 0) { - return Enumerable.Empty(); - } - return new[] { - new RenderInvalidation( - Part, - Part.position + notes.Min(note => note.position), - Part.position + notes.Max(note => note.End)) - }; - } } public class SetPhonemeExpressionCommand : ExpCommand { @@ -336,8 +317,6 @@ public class SetCurveCommand : ExpCommand { readonly int lastY; int[] oldXs; int[] oldYs; - int StartTick => Math.Min(x, lastX); - int EndTick => Math.Max(x, lastX) + 1; public override ValidateOptions ValidateOptions => new ValidateOptions { SkipTiming = true, @@ -387,19 +366,14 @@ public override bool CanMerge(IList commands) { public override UCommand Merge(IList commands) { var first = commands.First() as SetCurveCommand; var last = commands.Last() as SetCurveCommand; - var curveCommands = commands.Cast().ToArray(); var curve = Part.curves.FirstOrDefault(c => c.abbr == abbr); curve.Simplify(); int[] newXs = curve?.xs.ToArray(); int[] newYs = curve?.ys.ToArray(); return new MergedSetCurveCommand( last.project, last.Part, last.abbr, - first.oldXs, first.oldYs, newXs, newYs, - startTick: curveCommands.Min(command => command.StartTick), - endTick: curveCommands.Max(command => command.EndTick)); + first.oldXs, first.oldYs, newXs, newYs); } - public override IEnumerable GetRenderInvalidations() => - new[] { new RenderInvalidation(Part, Part.position + StartTick, Part.position + EndTick) }; } public class MergedSetCurveCommand : ExpCommand { @@ -410,11 +384,8 @@ public class MergedSetCurveCommand : ExpCommand { readonly int[] newXs; readonly int[] newYs; readonly bool setReal; - readonly int startTick; - readonly int endTick; public MergedSetCurveCommand(UProject project, UVoicePart part, - string abbr, int[] oldXs, int[] oldYs, int[] newXs, int[] newYs, bool setReal = false, - int startTick = -1, int endTick = -1) : base(part) { + string abbr, int[] oldXs, int[] oldYs, int[] newXs, int[] newYs, bool setReal = false) : base(part) { this.project = project; this.abbr = abbr; this.oldXs = oldXs; @@ -422,8 +393,6 @@ public MergedSetCurveCommand(UProject project, UVoicePart part, this.newXs = newXs; this.newYs = newYs; this.setReal = setReal; - this.startTick = startTick >= 0 ? startTick : MergedStartTick(oldXs, newXs); - this.endTick = endTick >= 0 ? endTick : MergedEndTick(oldXs, newXs); } public override string ToString() => "Edit Curve"; public override void Execute() { @@ -458,20 +427,6 @@ public override void Unexecute() { private List? GetCurveYs(UCurve? curve) { return setReal ? curve?.realYs : curve?.ys; } - static int MergedStartTick(int[]? oldXs, int[]? newXs) { - return (oldXs ?? Array.Empty()) - .Concat(newXs ?? Array.Empty()) - .DefaultIfEmpty(0) - .Min(); - } - int MergedEndTick(int[]? oldXs, int[]? newXs) { - return (oldXs ?? Array.Empty()) - .Concat(newXs ?? Array.Empty()) - .DefaultIfEmpty(Part.Duration) - .Max() + 1; - } - public override IEnumerable GetRenderInvalidations() => - new[] { new RenderInvalidation(Part, Part.position + startTick, Part.position + endTick) }; } public class PasteCurveCommand : ExpCommand { @@ -536,11 +491,6 @@ public override void Unexecute() { curve.ys.AddRange(oldYs); } } - public override IEnumerable GetRenderInvalidations() { - int startTick = xs.DefaultIfEmpty(0).Min(); - int endTick = xs.DefaultIfEmpty(Part.Duration).Max() + 1; - return new[] { new RenderInvalidation(Part, Part.position + startTick, Part.position + endTick) }; - } } public class ClearCurveCommand : ExpCommand { @@ -572,7 +522,5 @@ public override void Unexecute() { curve.ys.AddRange(oldYs); } } - public override IEnumerable GetRenderInvalidations() => - new[] { new RenderInvalidation(Part, Part.position, Part.End) }; } } diff --git a/OpenUtau.Core/Commands/NoteCommands.cs b/OpenUtau.Core/Commands/NoteCommands.cs index e3c1fbe4d..85bde011f 100644 --- a/OpenUtau.Core/Commands/NoteCommands.cs +++ b/OpenUtau.Core/Commands/NoteCommands.cs @@ -19,17 +19,6 @@ public NoteCommand(UVoicePart part, IEnumerable notes) { Part = part; Notes = notes.ToArray(); } - public override IEnumerable GetRenderInvalidations() { - if (Notes.Length == 0) { - return Enumerable.Empty(); - } - return new[] { - new RenderInvalidation( - Part, - Part.position + Notes.Min(note => note.position), - Part.position + Notes.Max(note => note.End)) - }; - } } public class AddNoteCommand : NoteCommand { diff --git a/OpenUtau.Core/Commands/Notifications.cs b/OpenUtau.Core/Commands/Notifications.cs index d2f91e2a4..37e9f4318 100644 --- a/OpenUtau.Core/Commands/Notifications.cs +++ b/OpenUtau.Core/Commands/Notifications.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using OpenUtau.Core.Ustx; namespace OpenUtau.Core { @@ -224,36 +222,15 @@ public FocusNoteNotification(UPart part, UNote note) { public override string ToString() => $"Focus note {note.lyric} at {note.position}."; } - public class PreRenderPriority { - public readonly UVoicePart part; - public readonly int startTick; - public readonly int endTick; - - public PreRenderPriority(UVoicePart part, int startTick, int endTick) { - this.part = part; - this.startTick = startTick; - this.endTick = endTick; - } - } - public class PreRenderNotification : UNotification { - public readonly PreRenderPriority[] priorities; - public bool HasPriorityRange => priorities.Length > 0; - - public PreRenderNotification() { - priorities = Array.Empty(); - } + public readonly int focusTick; - public PreRenderNotification(IEnumerable priorities) { - this.priorities = priorities - .Where(priority => priority.endTick > priority.startTick) - .ToArray(); - part = this.priorities.FirstOrDefault()?.part; + public PreRenderNotification(UPart part = null, int focusTick = -1) { + this.part = part; + this.focusTick = focusTick; } - public override string ToString() => HasPriorityRange - ? $"Pre-render notification {priorities.Length} prioritized range(s)." - : $"Pre-render notification."; + public override string ToString() => "Pre-render notification."; } public class PartRenderedNotification : UNotification { diff --git a/OpenUtau.Core/Commands/PartCommands.cs b/OpenUtau.Core/Commands/PartCommands.cs index fe6ff0b19..43debf786 100644 --- a/OpenUtau.Core/Commands/PartCommands.cs +++ b/OpenUtau.Core/Commands/PartCommands.cs @@ -1,7 +1,5 @@ using OpenUtau.Core.Ustx; using SharpCompress; -using System.Collections.Generic; -using System.Linq; namespace OpenUtau.Core { public abstract class PartCommand : UCommand { @@ -14,12 +12,6 @@ public PartCommand(UProject project, UPart part) { this.project = project; this.part = part; } - public override IEnumerable GetRenderInvalidations() { - if (part is UVoicePart voicePart) { - return new[] { new RenderInvalidation(voicePart, voicePart.position, voicePart.End) }; - } - return Enumerable.Empty(); - } } public class AddPartCommand : PartCommand { diff --git a/OpenUtau.Core/Commands/RenderInvalidation.cs b/OpenUtau.Core/Commands/RenderInvalidation.cs deleted file mode 100644 index 38427ee9a..000000000 --- a/OpenUtau.Core/Commands/RenderInvalidation.cs +++ /dev/null @@ -1,17 +0,0 @@ -using OpenUtau.Core.Ustx; - -namespace OpenUtau.Core { - public readonly struct RenderInvalidation { - public readonly UVoicePart part; - public readonly int startTick; - public readonly int endTick; - - public RenderInvalidation(UVoicePart part, int startTick, int endTick) { - this.part = part; - this.startTick = startTick; - this.endTick = endTick; - } - - public bool IsValid => part != null && endTick > startTick; - } -} diff --git a/OpenUtau.Core/Commands/UCommand.cs b/OpenUtau.Core/Commands/UCommand.cs index 255eacde9..a73eaf489 100644 --- a/OpenUtau.Core/Commands/UCommand.cs +++ b/OpenUtau.Core/Commands/UCommand.cs @@ -10,8 +10,6 @@ public abstract class UCommand { public abstract void Unexecute(); public virtual bool CanMerge(IList commands) => false; public virtual UCommand Merge(IList commands) => throw new NotImplementedException(); - public virtual IEnumerable GetRenderInvalidations() => - Enumerable.Empty(); public abstract override string ToString(); } diff --git a/OpenUtau.Core/DocManager.cs b/OpenUtau.Core/DocManager.cs index 5fa485ba9..22fb2fbba 100644 --- a/OpenUtau.Core/DocManager.cs +++ b/OpenUtau.Core/DocManager.cs @@ -278,31 +278,10 @@ public void EndUndoGroup() { if (undoGroup.DeferValidate) { Project.ValidateFull(); } - var preRenderNotification = CreatePreRenderNotification(undoGroup.Commands); undoGroup.Merge(); undoGroup = null; Log.Information("undoGroup ended"); - ExecuteCmd(preRenderNotification); - } - - PreRenderNotification CreatePreRenderNotification(IEnumerable commands) { - var priorityRanges = new Dictionary(); - foreach (var invalidation in commands.SelectMany(command => command.GetRenderInvalidations())) { - if (!invalidation.IsValid || !Project.parts.Contains(invalidation.part)) { - continue; - } - if (priorityRanges.TryGetValue(invalidation.part, out var range)) { - priorityRanges[invalidation.part] = ( - Math.Min(range.startTick, invalidation.startTick), - Math.Max(range.endTick, invalidation.endTick)); - } else { - priorityRanges[invalidation.part] = (invalidation.startTick, invalidation.endTick); - } - } - return priorityRanges.Count > 0 - ? new PreRenderNotification(priorityRanges.Select(range => - new PreRenderPriority(range.Key, range.Value.startTick, range.Value.endTick))) - : new PreRenderNotification(); + ExecuteCmd(new PreRenderNotification()); } public void RollBackUndoGroup() { @@ -335,7 +314,7 @@ public void Undo() { Publish(cmd, true); } redoQueue.AddToBack(group); - ExecuteCmd(CreatePreRenderNotification(group.Commands)); + ExecuteCmd(new PreRenderNotification()); } public void Redo() { @@ -352,7 +331,7 @@ public void Redo() { Publish(cmd); } undoQueue.AddToBack(group); - ExecuteCmd(CreatePreRenderNotification(group.Commands)); + ExecuteCmd(new PreRenderNotification()); } public bool GetUndoState(out string? key) { diff --git a/OpenUtau.Core/PlaybackManager.cs b/OpenUtau.Core/PlaybackManager.cs index fc1367bef..4789ee46d 100644 --- a/OpenUtau.Core/PlaybackManager.cs +++ b/OpenUtau.Core/PlaybackManager.cs @@ -188,6 +188,8 @@ private PlaybackManager() { double startMs; public int StartTick => DocManager.Inst.Project.timeAxis.MsPosToTickPos(startMs); CancellationTokenSource renderCancellation; + UVoicePart preRenderFocusPart; + int preRenderFocusTick = -1; public Audio.IAudioOutput AudioOutput { get; set; } = new Audio.DummyAudioOutput(); public bool OutputActive => AudioOutput.PlaybackState == PlaybackState.Playing; @@ -373,13 +375,12 @@ private void CheckFileWritable(string filePath) { } } - void SchedulePreRender(PreRenderNotification? notification = null) { - Log.Information(notification?.HasPriorityRange == true - ? $"SchedulePreRender {notification.priorities.Length} prioritized range(s)" - : "SchedulePreRender"); - var engine = notification?.HasPriorityRange == true - ? new RenderEngine(DocManager.Inst.Project, priorityRanges: notification.priorities) - : new RenderEngine(DocManager.Inst.Project); + void SchedulePreRender() { + Log.Information("SchedulePreRender"); + var engine = new RenderEngine( + DocManager.Inst.Project, + focusPart: preRenderFocusPart, + focusTick: preRenderFocusTick); engine.PreRenderProject(ref renderCancellation); } @@ -404,11 +405,28 @@ public void OnNext(UCommand cmd, bool isUndo) { } else if (cmd is LoadProjectNotification) { StopPlayback(); renderCancellation?.Cancel(); + preRenderFocusPart = null; + preRenderFocusTick = -1; DocManager.Inst.ExecuteCmd(new SetPlayPosTickNotification(0)); + } else if (cmd is LoadPartNotification loadPart) { + preRenderFocusPart = loadPart.part as UVoicePart; + preRenderFocusTick = loadPart.tick; + } else if (cmd is FocusNoteNotification focusNote) { + preRenderFocusPart = focusNote.part as UVoicePart; + preRenderFocusTick = focusNote.part?.position + focusNote.note.position ?? preRenderFocusTick; + } else if (cmd is SetPlayPosTickNotification setPlayPosTick) { + preRenderFocusTick = setPlayPosTick.playPosTick; + } else if (cmd is PreRenderNotification preRender) { + if (preRender.part is UVoicePart voicePart) { + preRenderFocusPart = voicePart; + } + if (preRender.focusTick >= 0) { + preRenderFocusTick = preRender.focusTick; + } } if (cmd is PreRenderNotification || cmd is LoadProjectNotification) { if (Util.Preferences.Default.PreRender) { - SchedulePreRender(cmd as PreRenderNotification); + SchedulePreRender(); } } } diff --git a/OpenUtau.Core/Render/RenderEngine.cs b/OpenUtau.Core/Render/RenderEngine.cs index 140ddda25..8319a8439 100644 --- a/OpenUtau.Core/Render/RenderEngine.cs +++ b/OpenUtau.Core/Render/RenderEngine.cs @@ -47,21 +47,22 @@ class RenderEngine { readonly int startTick; readonly int endTick; readonly int trackNo; - readonly PreRenderPriority[] priorityRanges; + readonly UVoicePart focusPart; + readonly int focusTick; public RenderEngine( UProject project, int startTick = 0, int endTick = -1, int trackNo = -1, - IEnumerable? priorityRanges = null) { + UVoicePart focusPart = null, + int focusTick = -1) { this.project = project; this.startTick = startTick; this.endTick = endTick; this.trackNo = trackNo; - this.priorityRanges = priorityRanges? - .Where(priority => priority.endTick > priority.startTick) - .ToArray() ?? Array.Empty(); + this.focusPart = focusPart; + this.focusTick = focusTick; } // for playback or export @@ -236,7 +237,7 @@ private void RenderRequests( } if (playing) { tuples = OrderForPlayback(tuples); - } else if (priorityRanges.Length > 0) { + } else if (focusPart != null || focusTick >= 0) { tuples = OrderForPreRender(tuples); } var progress = new Progress(tuples.Sum(t => t.Item1.phones.Length)); @@ -279,49 +280,30 @@ private void RenderRequests( (RenderPhrase phrase, WaveSource source, RenderPartRequest request)[] tuples) { return tuples .Select((tuple, index) => (tuple, index)) - .OrderBy(item => PreRenderPriorityBucket(item.tuple)) - .ThenBy(item => PreRenderPriorityIndex(item.tuple)) - .ThenBy(item => PreRenderPriorityDistance(item.tuple.phrase)) + .OrderBy(item => PreRenderAttentionBucket(item.tuple)) + .ThenBy(item => PreRenderAttentionDistance(item.tuple.phrase)) .ThenBy(item => item.index) .Select(item => item.tuple) .ToArray(); } - private int PreRenderPriorityBucket( + private int PreRenderAttentionBucket( (RenderPhrase phrase, WaveSource source, RenderPartRequest request) tuple) { - bool isPriorityPart = priorityRanges.Any(priority => ReferenceEquals(tuple.request.part, priority.part)); - bool overlapsPriority = priorityRanges.Any(priority => - ReferenceEquals(tuple.request.part, priority.part) && - RenderPriority.Overlaps(tuple.phrase.position, tuple.phrase.end, priority.startTick, priority.endTick)); - int earliestPriorityStart = priorityRanges.Min(priority => priority.startTick); + bool isPriorityPart = focusPart != null && ReferenceEquals(tuple.request.part, focusPart); + bool overlapsPriority = focusTick >= 0 && + tuple.phrase.position <= focusTick && + tuple.phrase.end > focusTick; + bool isAfterPriorityStart = focusTick < 0 || tuple.phrase.end > focusTick; return RenderPriority.PreRenderBucket( isPriorityPart, overlapsPriority, - tuple.phrase.end > earliestPriorityStart); + isAfterPriorityStart); } - private int PreRenderPriorityIndex( - (RenderPhrase phrase, WaveSource source, RenderPartRequest request) tuple) { - for (int i = 0; i < priorityRanges.Length; ++i) { - var priority = priorityRanges[i]; - if (ReferenceEquals(tuple.request.part, priority.part) && - RenderPriority.Overlaps(tuple.phrase.position, tuple.phrase.end, priority.startTick, priority.endTick)) { - return i; - } - } - for (int i = 0; i < priorityRanges.Length; ++i) { - if (ReferenceEquals(tuple.request.part, priorityRanges[i].part)) { - return i; - } - } - return int.MaxValue; - } - - private int PreRenderPriorityDistance(RenderPhrase phrase) { - return priorityRanges - .Select(priority => RenderPriority.PreRenderDistance(phrase.position, phrase.end, priority.startTick)) - .DefaultIfEmpty(0) - .Min(); + private int PreRenderAttentionDistance(RenderPhrase phrase) { + return focusTick >= 0 + ? RenderPriority.PreRenderDistance(phrase.position, phrase.end, focusTick) + : 0; } public static void ReleaseSourceTemp() { diff --git a/OpenUtau.Core/Render/RenderPriority.cs b/OpenUtau.Core/Render/RenderPriority.cs index 54a2619bf..f7e018946 100644 --- a/OpenUtau.Core/Render/RenderPriority.cs +++ b/OpenUtau.Core/Render/RenderPriority.cs @@ -19,10 +19,6 @@ internal static double PlaybackDistance(double sourceStartMs, double sourceEndMs return playbackStartMs - sourceEndMs; } - internal static bool Overlaps(int startTick, int endTick, int priorityStartTick, int priorityEndTick) { - return endTick > priorityStartTick && startTick < priorityEndTick; - } - internal static int PreRenderBucket( bool isPriorityPart, bool overlapsPriority, diff --git a/OpenUtau.Test/Core/Commands/RenderInvalidationTest.cs b/OpenUtau.Test/Core/Commands/RenderInvalidationTest.cs deleted file mode 100644 index 30d4aae1c..000000000 --- a/OpenUtau.Test/Core/Commands/RenderInvalidationTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Linq; -using OpenUtau.Core.Ustx; -using Xunit; - -namespace OpenUtau.Core { - public class RenderInvalidationTest { - [Fact] - public void NoteCommandReportsNoteRangeInProjectTicks() { - var part = new UVoicePart { position = 100, Duration = 1000 }; - var note = new UNote { position = 200, duration = 300 }; - var command = new AddNoteCommand(part, note); - - var invalidation = Assert.Single(command.GetRenderInvalidations()); - - Assert.Same(part, invalidation.part); - Assert.Equal(300, invalidation.startTick); - Assert.Equal(600, invalidation.endTick); - } - - [Fact] - public void SetCurveCommandReportsEditedRangeInProjectTicks() { - var project = new UProject(); - var part = new UVoicePart { position = 100, Duration = 1000 }; - var command = new SetCurveCommand( - project, part, Format.Ustx.PITD, x: 50, y: 0, lastX: 100, lastY: 0); - - var invalidation = Assert.Single(command.GetRenderInvalidations()); - - Assert.Same(part, invalidation.part); - Assert.Equal(150, invalidation.startTick); - Assert.Equal(201, invalidation.endTick); - } - - [Fact] - public void MergedSetCurveCommandKeepsMergedEditRange() { - var project = new UProject(); - var part = new UVoicePart { position = 100, Duration = 1000 }; - var command = new MergedSetCurveCommand( - project, - part, - Format.Ustx.PITD, - oldXs: new[] { 0, 400 }, - oldYs: new[] { 0, 0 }, - newXs: new[] { 0, 400 }, - newYs: new[] { 0, 0 }, - startTick: 50, - endTick: 101); - - var invalidation = Assert.Single(command.GetRenderInvalidations()); - - Assert.Equal(150, invalidation.startTick); - Assert.Equal(201, invalidation.endTick); - } - } -} diff --git a/OpenUtau.Test/Core/Render/RenderPriorityTest.cs b/OpenUtau.Test/Core/Render/RenderPriorityTest.cs index 19b9e7180..0a02791f1 100644 --- a/OpenUtau.Test/Core/Render/RenderPriorityTest.cs +++ b/OpenUtau.Test/Core/Render/RenderPriorityTest.cs @@ -3,21 +3,21 @@ namespace OpenUtau.Core.Render { public class RenderPriorityTest { [Fact] - public void PlaybackBucketPrioritizesCurrentThenFutureThenPast() { + public void PlaybackBucket_PrioritizesCurrentThenFutureThenPast() { Assert.Equal(0, RenderPriority.PlaybackBucket(100, 200, 150)); Assert.Equal(1, RenderPriority.PlaybackBucket(200, 300, 150)); Assert.Equal(2, RenderPriority.PlaybackBucket(0, 100, 150)); } [Fact] - public void OverlapsUsesHalfOpenRanges() { - Assert.True(RenderPriority.Overlaps(10, 20, 19, 30)); - Assert.False(RenderPriority.Overlaps(10, 20, 20, 30)); - Assert.False(RenderPriority.Overlaps(10, 20, 0, 10)); + public void PlaybackDistance_PrioritizesEarlierOffsetInCurrentBucket() { + Assert.Equal(50, RenderPriority.PlaybackDistance(100, 200, 150)); + Assert.Equal(50, RenderPriority.PlaybackDistance(200, 300, 150)); + Assert.Equal(50, RenderPriority.PlaybackDistance(0, 100, 150)); } [Fact] - public void PreRenderBucketPrioritizesOverlappingPriorityPart() { + public void PreRenderBucket_PrioritizesFocusedPartAtAttentionTick() { Assert.Equal(0, RenderPriority.PreRenderBucket( isPriorityPart: true, overlapsPriority: true, isAfterPriorityStart: true)); Assert.Equal(1, RenderPriority.PreRenderBucket( @@ -29,10 +29,10 @@ public void PreRenderBucketPrioritizesOverlappingPriorityPart() { } [Fact] - public void PreRenderDistancePrioritizesPhraseContainingPriorityStart() { + public void PreRenderDistance_PrioritizesPhraseContainingAttentionTick() { Assert.Equal(0, RenderPriority.PreRenderDistance(100, 200, 150)); Assert.Equal(50, RenderPriority.PreRenderDistance(200, 300, 150)); Assert.Equal(50, RenderPriority.PreRenderDistance(0, 100, 150)); } } -} +} \ No newline at end of file