FMovieSceneInverseSequenceTransform::TransformFiniteRangeWithinRange incorrect for looping transforms

When using FMovieSceneInverseSequenceTransform::TransformFiniteRangeWithinRange on the inverse of a looping transform, incorrect ranges are visited. This appears to be an issue with how that function handles partial loops included at the start or end of the inner range. See the repro steps for more detail.

Steps to Reproduce

When using FMovieSceneInverseSequenceTransform::TransformFiniteRangeWithinRange on the inverse of a looping transform, incorrect ranges are visited. I’ve attached a commandlet class that reproduces this issue in Unreal 5.7.0, the gist of which is reproduced below:

#include "Evaluation/MovieSceneSequenceTransform.h"
#include "MovieSceneTransformTypes.h"
#include "Sections/MovieSceneSectionTimingParameters.h"
 
 
int32 ULoopingTransformCommandlet::Main(const FString&)
{
	const FFrameRate TickResolution; // Default 60000 ticks per second (and we'll use the same for inner and outer)
 
	// Outer range is [0, 180000), i.e. [0, 3) seconds
	const TRange OuterRange(TRangeBound<FFrameNumber>::Inclusive(0), TRangeBound<FFrameNumber>::Exclusive(180000));
 
	// Inner range is [0, 60000), i.e. [0, 1) seconds
	const TRange InnerRange(TRangeBound<FFrameNumber>::Inclusive(0), TRangeBound<FFrameNumber>::Exclusive(60000));
 
	FMovieSceneSectionTimingParametersFrames TimingParams;
	TimingParams.bLoop = true;
	TimingParams.FirstLoopStartOffset = FFrameNumber(30000); // Start of section enters half-way through the loop
 
	const FMovieSceneSequenceTransform Transform = TimingParams.MakeTransform(TickResolution, OuterRange,
		TickResolution, InnerRange);
	const FMovieSceneInverseSequenceTransform InverseTransform = Transform.Inverse();
 
	// The transform should map these four ranges of the outer 3 second section:
	//
	// |<-------------------------------->||<-------------------------------->||<-------------------------------->|
	// [    0, 30000    )
	//                   [           30000, 90000           )
	//                                                       [           90000, 150000          )
	//                                                                                           [ 150000, 180000 )
	//
	// ... onto these ranges of the inner 1 second section:
	//
	// |<-------------------------------->|
	//                   [  30000, 60000  )
	// [               0, 60000           )
	// [               0, 60000           )
	// [    0, 30000    )
 
// Some test code omitted for brevity. See full commandlet class in uploaded .zip
 
	// This should log the four ranges in the outer section that can be mapped onto the inner section:
	//
	// |<-------------------------------->||<-------------------------------->||<-------------------------------->|
	// [    0, 30000    )
	//                   [           30000, 90000           )
	//                                                       [           90000, 150000          )
	//                                                                                           [ 150000, 180000 )
	//
	// But in my testing, the first range is omitted and the last range incorrectly has an open upper bound.
	// i.e. we expect to see: [0,30000), [30000,90000), [90000,150000), [150000,180000)
	//      but actually see:            [30000,90000), [90000,150000), [150000,+inf]
	InverseTransform.TransformFiniteRangeWithinRange(TraversedHull, LogFrameTimeRangeAndReturnTrue,
		StartBreadcrumbs, EndBreadcrumbs);
 
	return 0;
}

Reattaching the repro zip file, as it does not appear to be attached to the above message.

Hey folks, I’m going to pass this case over to Ludo here. He has been discussing this with the rest of the dev team. Apologies for the delay, with the holiday we weren’t able to respond right away.

Dustin

Hi! Yeah we’re figuring out what exactly this function should be returning in this case… what is your use-case for using this function? Like, what are you going to do with the returned time ranges?