OXYGENHUB
◆ main openmsx / src / imgui / timeline.hh
6.0 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#ifndef TIMELINE_HH
2
#define TIMELINE_HH
3
4
#include <algorithm>
5
#include <cmath>
6
#include <concepts>
7
#include <optional>
8
#include <span>
9
10
namespace openmsx {
11
12
// concept for types that have a "time" member of type Time
13
template<typename T, typename Time>
14
concept HasTime = requires(T t) {
15
	{ t.time } -> std::convertible_to<Time>;
16
};
17
18
// concept for timeline coordinate mapper (time <-> screen space)
19
template<typename T, typename Time>
20
concept TimelineMapper = requires(T c, Time time, float x) {
21
	{ c.timeToX(time) } -> std::convertible_to<float>;
22
	{ c.xToTime(x) } -> std::convertible_to<Time>;
23
};
24
25
/**
26
 * Classifies and processes timeline events as either DETAILED or COARSE regions.
27
 *
28
 * Motivation: When drawing millions of events on a limited screen (e.g., 1000 pixels wide),
29
 * many events will be smaller than a single pixel. This algorithm efficiently handles this by:
30
 * - Drawing individual events where there's space (DETAILED regions)
31
 * - Grouping dense clusters into coarse representations (COARSE regions)
32
 * This allows rendering large datasets (e.g., 10M events) efficiently on any zoom level.
33
 *
34
 * This function uses a streaming algorithm to efficiently process events on a zoomable timeline,
35
 * deciding for each event whether it should be processed individually (DETAILED) or grouped into
36
 * a coarse region (COARSE) based on available screen space.
37
 *
38
 * Algorithm:
39
 * - Iterates through events in a single pass (streaming)
40
 * - For each event, calculates its screen-space width (spacing to next event)
41
 * - If spacing > threshold: process event as DETAILED
42
 * - If spacing <= threshold: accumulates into a COARSE region and uses binary search
43
 *   to jump to the next pixel boundary, skipping dense clusters efficiently
44
 * - Merges adjacent coarse regions into single callback invocations
45
 *
46
 * Time Complexity: O(R log E)
47
 * - R = number of output regions (detailed + coarse)
48
 * - E = number of events
49
 * - R is bounded by: min(E, X) where X = number of screen pixels covered
50
 * - Best case: R = E (all events well-spaced, all detailed)
51
 * - Worst case: R = X (dense events, one coarse region per pixel)
52
 * - The binary search allows efficient skipping of dense event clusters
53
 *
54
 * @tparam Time       Time type (e.g., int, std::chrono::duration, EmuTime)
55
 * @tparam Event      Event type (must satisfy HasTime<Event, Time> concept)
56
 * @tparam Mapper     Mapper type (must satisfy TimelineMapper<Mapper, Time> concept)
57
 * @param events      Sorted list of events (must be sorted by time, ascending)
58
 * @param endTime     End time of the visible timeline (must be >= last event time)
59
 * @param mapper      Coordinate mapper (time <-> screen space)
60
 * @param onDetailed  Callable invoked for detailed events: (float fromX, float toX, const Event&)
61
 *                    - Called when an event has > threshold pixels of space
62
 *                    - [fromX, toX) defines the half-open interval in screen space
63
 * @param onCoarse    Callable invoked for coarse regions: (float fromX, float toX)
64
 *                    - Called when multiple events are densely packed (spacing <= threshold)
65
 *                    - Adjacent coarse pixels are automatically merged into single calls
66
 *                    - [fromX, toX) defines the half-open interval in screen space
67
 * @param threshold   Minimum spacing (in pixels) to process event as detailed (default: 1.0)
68
 *                    - spacing > threshold → DETAILED
69
 *                    - spacing <= threshold → COARSE
70
 *
71
 * Preconditions:
72
 * - events must be sorted by time (ascending order)
73
 * - endTime >= events.back().time (if events not empty)
74
 */
75
template<typename Time, HasTime<Time> Event, TimelineMapper<Time> Mapper>
76
void processTimeline(
77
	std::span<const Event> events,
78
	Time endTime,
79
	const Mapper& mapper,
80
	std::invocable<float, float, const Event&> auto onDetailed,
81
	std::invocable<float, float, typename std::span<const Event>::iterator, typename std::span<const Event>::iterator> auto onCoarse,
82
	float threshold = 1.0)
83
{
84
	if (events.empty()) return;
85
86
	std::optional<float> coarseStart; // start of current coarse region
87
	decltype(events.begin()) coarseBeginIt;
88
	auto flushCoarse = [&](float endPixel, auto endIt) {
89
		if (coarseStart) {
90
			onCoarse(*coarseStart, endPixel, coarseBeginIt, endIt);
91
			coarseStart = {};
92
			coarseBeginIt = {};
93
		}
94
	};
95
96
	auto currentIt = events.begin();
97
	auto currentPixel = mapper.timeToX(currentIt->time);
98
	while (currentIt != events.end()) {
99
		// get next time (either next event or endTime)
100
		const auto nextIt = std::next(currentIt);
101
		const auto nextTimestamp = (nextIt != events.end())
102
		                         ? nextIt->time
103
		                         : endTime;
104
		const auto nextPixel = mapper.timeToX(nextTimestamp);
105
		const auto spacing = nextPixel - currentPixel; // event spans [currentPixel, nextPixel)
106
107
		if (spacing > threshold) {
108
			// DETAILED: flush pending coarse region first
109
			flushCoarse(currentPixel, nextIt);
110
			onDetailed(currentPixel, nextPixel, *currentIt);
111
			currentIt = nextIt; // reuse for next iteration
112
			currentPixel = nextPixel;
113
		} else {
114
			// COARSE: start/continue coarse region
115
			if (!coarseStart) {
116
				coarseStart = currentPixel;
117
				coarseBeginIt = currentIt;
118
			}
119
120
			// binary search: jump to next integer pixel boundary
121
			const auto targetPixel = std::floor(currentPixel) + 1.0f;
122
			const auto targetTime = mapper.xToTime(targetPixel);
123
124
			// Find last event at or before the next pixel boundary.
125
			// upper_bound gives first event > targetTime, then back up one.
126
			// Important: Start from next event to ensure we make progress.
127
			if (auto it = std::ranges::upper_bound(nextIt, events.end(), targetTime, {}, &Event::time);
128
			    it != nextIt) {
129
				currentIt = std::prev(it);
130
				currentPixel = mapper.timeToX(currentIt->time);
131
			} else {
132
				// no more events in this pixel, jump to next event
133
				currentIt = nextIt;
134
				currentPixel = nextPixel;
135
			}
136
		}
137
	}
138
139
	// flush final coarse region if any
140
	flushCoarse(currentPixel, currentIt); // at this point: currentPixel == mapper.timeToX(endTime)
141
}
142
143
} // namespace openmsx
144
145
#endif