OXYGENHUB
◆ main enhanced-x16-emulator / src / perf_budget.c
25.8 KiB · C · last changed feat: the RRDC control port (-controlport), contract 0.6: GET/POST /perf from the Performance panel's measurements, built by perf_json.c with DAP's x16/perfStats; a guest marker region named in advance; a framed guest's regions carried across the frame boundary and an interrupt after $F1 counted as work (#1) 25 days ago source blame history raw
1
// Commander X16 Emulator — guest performance budget accounting.
2
// See perf_budget.h for what this measures and why it has to classify cycles
3
// rather than merely count them.
4
5
#include "perf_budget.h"
6
7
#include <stdlib.h>
8
#include <string.h>
9
#include <stdio.h>
10
11
// Rounding and absolute value, done by hand rather than with <math.h>.
12
//
13
// perf_budget.c is linked into unit tests that link nothing else -- that is the
14
// whole point of it taking the machine as arguments -- and it is also linked
15
// into memory.c's test targets. lrintf() and fabsf() are intrinsics on MSVC but
16
// real libm symbols on glibc, so using them meant every one of those targets
17
// had to grow a -lm it does not otherwise need. Both values rounded here are
18
// positive, so the naive form is exact and obviously correct.
19
static uint32_t
20
round_pos(float v)
21
{
22
	return (uint32_t)(v + 0.5f);
23
}
24
25
static float
26
abs_f(float v)
27
{
28
	return v < 0.0f ? -v : v;
29
}
30
31
bool perf_budget_enabled = false;
32
33
// ─── Configuration ──────────────────────────────────────────────────────────
34
35
static uint32_t cfg_machine_khz      = 8000;
36
static uint32_t cfg_cycles_per_frame = 134400; // 8 MHz, VGA
37
static uint16_t cfg_scanlines        = 525;
38
static float    cfg_target_fps       = 60.0f;
39
40
static perf_idle_mode_t cfg_idle_mode = PERF_IDLE_AUTO;
41
42
// Derived from the above by recompute_budget().
43
static int      budget_frames = 1;      // vsyncs the budget spans
44
static uint32_t budget_cycles = 134400;
45
46
// ─── The ring ───────────────────────────────────────────────────────────────
47
48
static perf_frame_t *ring     = NULL;
49
static uint32_t     *scratch  = NULL; // percentile workspace, sized with the ring
50
static int           capacity = PERF_DEFAULT_CAPACITY;
51
static int           head     = 0; // next slot to write
52
static int           count    = 0; // valid samples
53
static uint32_t      total_frames = 0;
54
55
// ─── The frame in progress ──────────────────────────────────────────────────
56
57
static perf_frame_t cur;
58
static uint32_t     cur_zone[PERF_MAX_ZONES];
59
60
// Trailing work history, for budgets that span more than one frame. Only the
61
// last `budget_frames` entries matter and budget_frames is small, so a tiny
62
// ring of its own is cheaper and simpler than reaching back into the main one
63
// (which may be empty, or may have been resized, exactly when this is needed).
64
#define PERF_PERIOD_MAX 16
65
static uint32_t period_hist[PERF_PERIOD_MAX];
66
static int      period_pos = 0;
67
68
// ─── Spin detection ─────────────────────────────────────────────────────────
69
70
static uint32_t store_count = 0;
71
72
static struct {
73
	uint16_t anchor;
74
	uint16_t lo, hi;
75
	uint16_t last_pc;
76
	uint32_t stores_at_anchor;
77
	uint32_t pending; // cycles in the pass currently being accumulated
78
	uint8_t  iters;   // clean passes completed
79
	bool     valid;
80
	bool     have_last;
81
} spin;
82
83
// ─── Markers ────────────────────────────────────────────────────────────────
84
85
static bool    marker_mode = false; // a marker has been seen this session
86
static bool    marker_idle = false; // $F1 seen; work resumes at $F0
87
static bool    marker_framed = false; // $F0 seen and no $F1 since: the guest's frame is open
88
static int8_t  marker_stack[PERF_MARKER_STACK];
89
static int     marker_depth = 0;
90
91
// ─── Zones ──────────────────────────────────────────────────────────────────
92
93
static perf_zone_t zones[PERF_MAX_ZONES];
94
static uint8_t    *zone_of = NULL; // PC -> zone index + 1; 0 means none
95
96
// ─── Overrun accumulator ────────────────────────────────────────────────────
97
98
static perf_overrun_report_t pending_overruns;
99
100
// ────────────────────────────────────────────────────────────────────────────
101
102
static void
103
recompute_budget(void)
104
{
105
	if (cfg_cycles_per_frame == 0)
106
		cfg_cycles_per_frame = 1;
107
108
	const float vsync = perf_budget_vsync_hz();
109
	if (cfg_target_fps <= 0.0f || vsync <= 0.0f) {
110
		budget_frames = 1;
111
		budget_cycles = cfg_cycles_per_frame;
112
		return;
113
	}
114
115
	// Vsyncs per unit of work. Above 1 the work may span several frames; below
116
	// 1 the caller is asking for a routine to fit inside part of one.
117
	const float ratio = vsync / cfg_target_fps;
118
	int         n     = (int)round_pos(ratio);
119
120
	if (n >= 1 && abs_f(ratio - (float)n) <= 0.05f * (float)n) {
121
		// Close enough to a whole number of vsyncs to be what was meant.
122
		if (n > PERF_PERIOD_MAX)
123
			n = PERF_PERIOD_MAX;
124
		budget_frames = n;
125
		budget_cycles = cfg_cycles_per_frame * (uint32_t)n;
126
	} else {
127
		// A fraction of a frame, or an awkward multiple of one. Sum over as
128
		// many whole frames as the ratio covers, and scale the budget exactly.
129
		if (n < 1)
130
			n = 1;
131
		if (n > PERF_PERIOD_MAX)
132
			n = PERF_PERIOD_MAX;
133
		budget_frames = n;
134
		budget_cycles = round_pos((float)cfg_cycles_per_frame * ratio);
135
		if (budget_cycles == 0)
136
			budget_cycles = 1;
137
	}
138
}
139
140
// What a period's work is actually compared against: the budget, plus the
141
// boundary granularity described at PERF_BOUNDARY_SLACK.
142
static uint32_t
143
overrun_threshold(void)
144
{
145
	return budget_cycles + (uint32_t)budget_frames * PERF_BOUNDARY_SLACK;
146
}
147
148
static void
149
spin_reset(uint16_t pc, bool valid)
150
{
151
	spin.anchor           = pc;
152
	spin.lo               = pc;
153
	spin.hi               = pc;
154
	spin.stores_at_anchor = store_count;
155
	spin.pending          = 0;
156
	spin.iters            = 0;
157
	spin.valid            = valid;
158
}
159
160
static void
161
clear_current(void)
162
{
163
	memset(&cur, 0, sizeof cur);
164
	memset(cur_zone, 0, sizeof cur_zone);
165
	spin_reset(0, false);
166
	spin.have_last = false;
167
}
168
169
void
170
perf_budget_configure(uint32_t machine_khz, uint32_t cycles_per_frame, uint16_t scanlines_per_frame)
171
{
172
	if (machine_khz)
173
		cfg_machine_khz = machine_khz;
174
	if (cycles_per_frame)
175
		cfg_cycles_per_frame = cycles_per_frame;
176
	if (scanlines_per_frame)
177
		cfg_scanlines = scanlines_per_frame;
178
	recompute_budget();
179
}
180
181
void
182
perf_budget_set_target_fps(float fps)
183
{
184
	if (fps > 0.0f)
185
		cfg_target_fps = fps;
186
	recompute_budget();
187
}
188
189
float
190
perf_budget_get_target_fps(void)
191
{
192
	return cfg_target_fps;
193
}
194
195
float
196
perf_budget_vsync_hz(void)
197
{
198
	if (cfg_cycles_per_frame == 0)
199
		return 0.0f;
200
	return (float)cfg_machine_khz * 1000.0f / (float)cfg_cycles_per_frame;
201
}
202
203
uint32_t
204
perf_budget_cycles_per_frame(void)
205
{
206
	return cfg_cycles_per_frame;
207
}
208
209
uint32_t
210
perf_budget_cycles_per_scanline(void)
211
{
212
	return cfg_scanlines ? cfg_cycles_per_frame / cfg_scanlines : 0;
213
}
214
215
uint32_t
216
perf_budget_budget_cycles(void)
217
{
218
	return budget_cycles;
219
}
220
221
int
222
perf_budget_frames_per_budget(void)
223
{
224
	return budget_frames;
225
}
226
227
void
228
perf_budget_set_idle_mode(perf_idle_mode_t mode)
229
{
230
	cfg_idle_mode = mode;
231
}
232
233
perf_idle_mode_t
234
perf_budget_get_idle_mode(void)
235
{
236
	return cfg_idle_mode;
237
}
238
239
// ─── Enable / capacity ──────────────────────────────────────────────────────
240
241
static bool
242
alloc_ring(int frames)
243
{
244
	perf_frame_t *nr = (perf_frame_t *)calloc((size_t)frames, sizeof(perf_frame_t));
245
	if (!nr)
246
		return false;
247
	uint32_t *ns = (uint32_t *)calloc((size_t)frames, sizeof(uint32_t));
248
	if (!ns) {
249
		free(nr);
250
		return false;
251
	}
252
	free(ring);
253
	free(scratch);
254
	ring     = nr;
255
	scratch  = ns;
256
	capacity = frames;
257
	head     = 0;
258
	count    = 0;
259
	return true;
260
}
261
262
bool
263
perf_budget_set_enabled(bool enabled)
264
{
265
	if (!enabled) {
266
		perf_budget_enabled = false;
267
		free(ring);
268
		free(scratch);
269
		ring    = NULL;
270
		scratch = NULL;
271
		head = count = 0;
272
		clear_current();
273
		return true;
274
	}
275
276
	if (!ring && !alloc_ring(capacity))
277
		return false;
278
279
	clear_current();
280
	memset(period_hist, 0, sizeof period_hist);
281
	period_pos = 0;
282
	memset(&pending_overruns, 0, sizeof pending_overruns);
283
	perf_budget_enabled = true;
284
	return true;
285
}
286
287
bool
288
perf_budget_is_enabled(void)
289
{
290
	return perf_budget_enabled;
291
}
292
293
static bool arm_want[PERF_OWNER_COUNT];
294
295
bool
296
perf_budget_arm(perf_owner_t owner, bool want)
297
{
298
	if ((int)owner < 0 || (int)owner >= PERF_OWNER_COUNT)
299
		return false;
300
	arm_want[owner] = want;
301
302
	bool any = false;
303
	for (int i = 0; i < PERF_OWNER_COUNT; i++)
304
		any = any || arm_want[i];
305
306
	// Only act on a real change: perf_budget_set_enabled(true) restarts the
307
	// accumulator, so calling it every frame from a panel that is merely still
308
	// open would keep resetting the frame in progress.
309
	if (any == perf_budget_enabled)
310
		return true;
311
	return perf_budget_set_enabled(any);
312
}
313
314
bool
315
perf_budget_owner_wants(perf_owner_t owner)
316
{
317
	if ((int)owner < 0 || (int)owner >= PERF_OWNER_COUNT)
318
		return false;
319
	return arm_want[owner];
320
}
321
322
bool
323
perf_budget_set_capacity(int frames)
324
{
325
	if (frames < 2)
326
		frames = 2;
327
	if (frames == capacity && ring)
328
		return true;
329
	if (!ring) { // not allocated yet; remember it for when profiling starts
330
		capacity = frames;
331
		return true;
332
	}
333
	return alloc_ring(frames);
334
}
335
336
int
337
perf_budget_capacity(void)
338
{
339
	return capacity;
340
}
341
342
void
343
perf_budget_reset(void)
344
{
345
	head = count = 0;
346
	total_frames = 0;
347
	clear_current();
348
	memset(period_hist, 0, sizeof period_hist);
349
	period_pos = 0;
350
	memset(&pending_overruns, 0, sizeof pending_overruns);
351
	store_count = 0;
352
353
	// Marker state goes too. Marker mode suppresses the heuristic entirely, so
354
	// leaving it set would mean a program that used markers once could never
355
	// get automatic detection back -- not by resetting, not by reloading, not
356
	// by anything short of restarting the emulator. Since a guest that is still
357
	// driving markers re-asserts the mode on its very next write, clearing it
358
	// costs that guest nothing and gives every other one its detector back.
359
	marker_mode   = false;
360
	marker_idle   = false;
361
	marker_framed = false;
362
	marker_depth  = 0;
363
}
364
365
// ─── The hot path ───────────────────────────────────────────────────────────
366
367
void
368
perf_budget_note_store_(void)
369
{
370
	store_count++;
371
}
372
373
// Track a candidate spin loop, and credit whole passes of a confirmed one to
374
// idle.
375
//
376
// A loop is identified by the BACKWARD JUMP that closes it, not by watching the
377
// PC drift within some window. That distinction matters: keying on drift means
378
// whatever address the walk happens to start at becomes the anchor, so a
379
// compact routine whose whole body fits inside the window is treated as one
380
// enormous "loop" that stores -- and the tight, store-free poll nested inside
381
// it is never seen at all. Anchoring on the branch target instead means each
382
// loop is recognised as itself, however tightly the code around it is packed.
383
//
384
// Classification happens at pass boundaries rather than per instruction so that
385
// a loop is never credited before it has proved itself; the cost is that the
386
// first confirmed pass is not counted, which is a handful of cycles against a
387
// frame of six figures.
388
static void
389
spin_track(uint32_t clocks, uint16_t pc, int irq_depth)
390
{
391
	// A handler is work by definition, and it also perturbs the loop it
392
	// interrupted, so re-anchor rather than trying to resume the pass.
393
	if (irq_depth > 0) {
394
		spin_reset(pc, false);
395
		spin.last_pc   = pc;
396
		spin.have_last = true;
397
		return;
398
	}
399
400
	if (!spin.valid)
401
		spin_reset(pc, true);
402
403
	const bool backward = spin.have_last && pc < spin.last_pc;
404
405
	if (backward) {
406
		if (pc == spin.anchor) {
407
			// The loop being watched has closed another pass.
408
			const bool clean = (spin.stores_at_anchor == store_count) &&
409
			                   ((uint16_t)(spin.hi - spin.lo) <= PERF_SPIN_SPAN);
410
			if (clean) {
411
				if (spin.iters >= 1)
412
					cur.idle += spin.pending;
413
				if (spin.iters < 255)
414
					spin.iters++;
415
			} else {
416
				spin.iters = 0;
417
			}
418
			spin.pending          = 0;
419
			spin.lo               = pc;
420
			spin.hi               = pc;
421
			spin.stores_at_anchor = store_count;
422
		} else {
423
			// A different backward jump: that target is the loop head now.
424
			spin_reset(pc, true);
425
		}
426
	} else {
427
		if (pc < spin.lo)
428
			spin.lo = pc;
429
		if (pc > spin.hi)
430
			spin.hi = pc;
431
	}
432
433
	spin.pending += clocks;
434
	spin.last_pc   = pc;
435
	spin.have_last = true;
436
}
437
438
void
439
perf_budget_step_(uint32_t clocks, uint16_t pc, uint8_t pbank, bool waiting, int irq_depth)
440
{
441
	cur.total += clocks;
442
	cur.instructions++;
443
	if (irq_depth > 0)
444
		cur.irq += clocks;
445
446
	// Attribution. An open marker region wins over an address zone: the guest
447
	// saying "this is the sprite update" is better evidence than a range
448
	// somebody drew around what they thought the sprite update was.
449
	int  zone   = -1;
450
	bool z_idle = false;
451
	if (marker_depth > 0) {
452
		zone = marker_stack[marker_depth - 1];
453
	} else if (zone_of) {
454
		const uint8_t t = zone_of[pc];
455
		if (t) {
456
			const perf_zone_t *z = &zones[t - 1];
457
			if (z->bank < 0 || z->bank == (int16_t)pbank)
458
				zone = t - 1;
459
		}
460
	}
461
	if (zone >= 0 && zone < PERF_MAX_ZONES) {
462
		cur_zone[zone] += clocks;
463
		z_idle = zones[zone].is_idle;
464
	}
465
466
	// Idle classification, highest precedence first.
467
	if (marker_mode) {
468
		// The guest is telling us. Do not also guess: an explicit statement
469
		// that beats the heuristic is the whole point of the marker layer.
470
		// Except inside an interrupt handler: the IRQ that lands in the wait
471
		// after $F1 is the CPU working, not waiting, and counting it idle
472
		// would make main (work - irq) come up short by the whole handler.
473
		if ((marker_idle || z_idle) && irq_depth == 0)
474
			cur.idle += clocks;
475
		spin_reset(pc, false);
476
	} else if (z_idle) {
477
		cur.idle += clocks;
478
		spin_reset(pc, false);
479
	} else if (waiting && cfg_idle_mode != PERF_IDLE_NONE) {
480
		cur.idle += clocks; // WAI: unambiguous, and free to detect
481
		spin_reset(pc, false);
482
	} else if (cfg_idle_mode == PERF_IDLE_AUTO) {
483
		spin_track(clocks, pc, irq_depth);
484
	} else {
485
		spin_reset(pc, false);
486
	}
487
488
	// A headless run has no video_step() to raise a frame, so nothing would
489
	// ever close the accumulator and the module would report one ever-growing
490
	// frame. Close it synthetically once it has run well past a frame's worth.
491
	// The threshold is deliberately generous: a machine that does produce
492
	// frames must never reach it, however badly it is overrunning its budget.
493
	if (cur.total >= cfg_cycles_per_frame * 4u)
494
		perf_budget_frame_end(0);
495
}
496
497
void
498
perf_budget_frame_end(uint32_t host_us)
499
{
500
	if (!perf_budget_enabled || !ring)
501
		return;
502
503
	// A region left open by the guest is closed here rather than carried, so a
504
	// missing leave costs one frame instead of every frame after it -- unless
505
	// the guest frames its work with $F0 ... $F1 and is inside that frame.
506
	// Then the region is the guest's own word that its work is still going:
507
	// a game's frame begins where IT waits, not at this boundary, so its work
508
	// routinely runs across it, and closing the region here would hand the
509
	// rest of whichever phase was running to no zone at all. Its next $F0 or
510
	// $F1 still closes everything, so a missing leave costs one of the
511
	// guest's frames.
512
	if (!marker_framed)
513
		marker_depth = 0;
514
515
	const uint32_t work = cur.total - cur.idle;
516
517
	period_hist[period_pos] = work;
518
	period_pos              = (period_pos + 1) % PERF_PERIOD_MAX;
519
520
	uint32_t period = 0;
521
	for (int i = 0; i < budget_frames; i++) {
522
		const int idx = (period_pos - 1 - i + 2 * PERF_PERIOD_MAX) % PERF_PERIOD_MAX;
523
		period += period_hist[idx];
524
	}
525
526
	cur.frame       = total_frames;
527
	cur.host_us     = host_us;
528
	cur.period_work = period;
529
	for (int i = 0; i < PERF_RING_ZONES; i++)
530
		cur.zone[i] = cur_zone[i];
531
532
	ring[head] = cur;
533
	head       = (head + 1) % capacity;
534
	if (count < capacity)
535
		count++;
536
	total_frames++;
537
538
	if (period > overrun_threshold()) {
539
		const uint32_t over = period - budget_cycles;
540
		pending_overruns.overruns++;
541
		if (over > pending_overruns.worst_overrun) {
542
			pending_overruns.worst_overrun = over;
543
			pending_overruns.worst_frame   = cur.frame;
544
			pending_overruns.worst_utilization =
545
			    budget_cycles ? 100.0f * (float)period / (float)budget_cycles : 0.0f;
546
		}
547
	}
548
	pending_overruns.frames++;
549
550
	clear_current();
551
}
552
553
// ─── Markers ────────────────────────────────────────────────────────────────
554
555
static int marker_zone(uint8_t id);
556
557
int
558
perf_budget_zone_add_marker(const char *name, uint8_t id)
559
{
560
	if (id < 1 || id > 0x3F)
561
		return -1;
562
	const int zi = marker_zone(id);
563
	if (zi < 0)
564
		return -1;
565
	if (name && *name)
566
		snprintf(zones[zi].name, sizeof zones[zi].name, "%s", name);
567
	return zi;
568
}
569
570
// Find (or claim) the zone slot standing for marker region `id`.
571
static int
572
marker_zone(uint8_t id)
573
{
574
	int free_slot = -1;
575
	for (int i = 0; i < PERF_MAX_ZONES; i++) {
576
		if (zones[i].used && zones[i].is_marker && zones[i].marker_id == id)
577
			return i;
578
		if (!zones[i].used && free_slot < 0)
579
			free_slot = i;
580
	}
581
	if (free_slot < 0)
582
		return -1; // table full: the region still counts as work, just unnamed
583
584
	perf_zone_t *z = &zones[free_slot];
585
	memset(z, 0, sizeof *z);
586
	snprintf(z->name, sizeof z->name, "region %u", (unsigned)id);
587
	z->bank      = -1;
588
	z->is_marker = true;
589
	z->marker_id = id;
590
	z->used      = true;
591
	return free_slot;
592
}
593
594
void
595
perf_budget_marker_write(uint8_t value)
596
{
597
	marker_mode = true;
598
599
	if (value == 0xFE) {
600
		perf_budget_reset();
601
		marker_mode = true; // the reset cleared it; the guest is still driving
602
		marker_idle = false;
603
		return;
604
	}
605
	if (value == 0xF0) {
606
		marker_idle   = false;
607
		marker_framed = true;
608
		marker_depth  = 0;
609
		return;
610
	}
611
	if (value == 0xF1) {
612
		marker_idle   = true;
613
		marker_framed = false;
614
		marker_depth  = 0;
615
		return;
616
	}
617
	if (value == 0x00) {
618
		if (marker_depth > 0)
619
			marker_depth--;
620
		return;
621
	}
622
	if (value & 0x80) {
623
		const uint8_t id = value & 0x3F;
624
		// Unwind to just below the named region. Leaving a region that is not
625
		// open is ignored rather than treated as an error.
626
		for (int d = marker_depth - 1; d >= 0; d--) {
627
			const int zi = marker_stack[d];
628
			if (zi >= 0 && zones[zi].is_marker && zones[zi].marker_id == id) {
629
				marker_depth = d;
630
				return;
631
			}
632
		}
633
		return;
634
	}
635
	if (value <= 0x3F) {
636
		if (marker_depth < PERF_MARKER_STACK)
637
			marker_stack[marker_depth++] = (int8_t)marker_zone(value);
638
		return;
639
	}
640
}
641
642
uint8_t
643
perf_budget_marker_read(void)
644
{
645
	return PERF_MARKER_PROTOCOL;
646
}
647
648
// ─── Zones ──────────────────────────────────────────────────────────────────
649
650
static void
651
rebuild_zone_map(void)
652
{
653
	bool any = false;
654
	for (int i = 0; i < PERF_MAX_ZONES; i++) {
655
		if (zones[i].used && !zones[i].is_marker) {
656
			any = true;
657
			break;
658
		}
659
	}
660
	if (!any) {
661
		free(zone_of);
662
		zone_of = NULL;
663
		return;
664
	}
665
	if (!zone_of) {
666
		zone_of = (uint8_t *)calloc(65536, 1);
667
		if (!zone_of)
668
			return;
669
	}
670
	memset(zone_of, 0, 65536);
671
672
	// Later zones win where two overlap, which makes "draw a tighter zone
673
	// inside a looser one" work the way anybody would expect.
674
	for (int i = 0; i < PERF_MAX_ZONES; i++) {
675
		const perf_zone_t *z = &zones[i];
676
		if (!z->used || z->is_marker || z->end <= z->start)
677
			continue;
678
		for (uint32_t a = z->start; a < z->end; a++)
679
			zone_of[a] = (uint8_t)(i + 1);
680
	}
681
}
682
683
int
684
perf_budget_zone_add(const char *name, uint16_t start, uint16_t end, int16_t bank, bool is_idle)
685
{
686
	if (end <= start)
687
		return -1;
688
	for (int i = 0; i < PERF_MAX_ZONES; i++) {
689
		if (zones[i].used)
690
			continue;
691
		perf_zone_t *z = &zones[i];
692
		memset(z, 0, sizeof *z);
693
		snprintf(z->name, sizeof z->name, "%s", name ? name : "zone");
694
		z->start   = start;
695
		z->end     = end;
696
		z->bank    = bank;
697
		z->is_idle = is_idle;
698
		z->used    = true;
699
		rebuild_zone_map();
700
		return i;
701
	}
702
	return -1;
703
}
704
705
bool
706
perf_budget_zone_remove(int index)
707
{
708
	if (index < 0 || index >= PERF_MAX_ZONES || !zones[index].used)
709
		return false;
710
	memset(&zones[index], 0, sizeof zones[index]);
711
	// Anything still pointing at the slot would attribute cycles to a zone that
712
	// no longer exists.
713
	for (int d = 0; d < marker_depth; d++)
714
		if (marker_stack[d] == index)
715
			marker_stack[d] = -1;
716
	rebuild_zone_map();
717
	return true;
718
}
719
720
void
721
perf_budget_zone_clear(void)
722
{
723
	memset(zones, 0, sizeof zones);
724
	memset(cur_zone, 0, sizeof cur_zone);
725
	marker_depth = 0;
726
	rebuild_zone_map();
727
}
728
729
int
730
perf_budget_zone_count(void)
731
{
732
	int n = 0;
733
	for (int i = 0; i < PERF_MAX_ZONES; i++)
734
		if (zones[i].used)
735
			n = i + 1;
736
	return n;
737
}
738
739
bool
740
perf_budget_zone_get(int index, perf_zone_t *out)
741
{
742
	if (index < 0 || index >= PERF_MAX_ZONES || !zones[index].used)
743
		return false;
744
	if (out)
745
		*out = zones[index];
746
	return true;
747
}
748
749
// ─── Queries ────────────────────────────────────────────────────────────────
750
751
static int
752
cmp_u32(const void *a, const void *b)
753
{
754
	const uint32_t x = *(const uint32_t *)a, y = *(const uint32_t *)b;
755
	return (x > y) - (x < y);
756
}
757
758
// Nearest-rank percentile over an ascending array.
759
static uint32_t
760
percentile(const uint32_t *v, int n, int p)
761
{
762
	if (n <= 0)
763
		return 0;
764
	int idx = (int)(((int64_t)p * n + 99) / 100) - 1;
765
	if (idx < 0)
766
		idx = 0;
767
	if (idx >= n)
768
		idx = n - 1;
769
	return v[idx];
770
}
771
772
// Consumes `scratch[0..n-1]` (unsorted on entry, sorted on exit).
773
static void
774
stat_from_scratch(int n, perf_stat_t *out)
775
{
776
	memset(out, 0, sizeof *out);
777
	if (n <= 0)
778
		return;
779
780
	uint64_t sum = 0;
781
	for (int i = 0; i < n; i++)
782
		sum += scratch[i];
783
784
	qsort(scratch, (size_t)n, sizeof(uint32_t), cmp_u32);
785
786
	out->min  = scratch[0];
787
	out->max  = scratch[n - 1];
788
	out->mean = (uint32_t)(sum / (uint64_t)n);
789
	out->p50  = percentile(scratch, n, 50);
790
	out->p95  = percentile(scratch, n, 95);
791
	out->p99  = percentile(scratch, n, 99);
792
}
793
794
// How many of the newest samples fall in `seconds`.
795
static int
796
window_frames(float seconds)
797
{
798
	if (count <= 0)
799
		return 0;
800
	if (seconds <= 0.0f)
801
		return count;
802
	const float hz = perf_budget_vsync_hz();
803
	if (hz <= 0.0f)
804
		return count;
805
	int n = (int)(seconds * hz + 0.5f);
806
	if (n < 1)
807
		n = 1;
808
	if (n > count)
809
		n = count;
810
	return n;
811
}
812
813
// Index of the i'th newest sample (i == 0 is the newest).
814
static int
815
newest(int i)
816
{
817
	return (head - 1 - i + 2 * capacity) % capacity;
818
}
819
820
bool
821
perf_budget_last_frame(perf_frame_t *out)
822
{
823
	if (!ring || count <= 0)
824
		return false;
825
	if (out)
826
		*out = ring[newest(0)];
827
	return true;
828
}
829
830
bool
831
perf_budget_current_frame(perf_frame_t *out)
832
{
833
	if (!out)
834
		return false;
835
	*out = cur;
836
	out->frame = total_frames;
837
	for (int i = 0; i < PERF_RING_ZONES; i++)
838
		out->zone[i] = cur_zone[i];
839
	out->period_work = cur.total - cur.idle;
840
	return true;
841
}
842
843
int
844
perf_budget_frame_count(void)
845
{
846
	return count;
847
}
848
849
bool
850
perf_budget_frame_at(int newest_index, perf_frame_t *out)
851
{
852
	if (!ring || newest_index < 0 || newest_index >= count)
853
		return false;
854
	if (out)
855
		*out = ring[newest(newest_index)];
856
	return true;
857
}
858
859
uint32_t
860
perf_budget_total_frames(void)
861
{
862
	return total_frames;
863
}
864
865
bool
866
perf_budget_window(float seconds, perf_window_t *out)
867
{
868
	if (!out || !ring)
869
		return false;
870
	const int n = window_frames(seconds);
871
	if (n <= 0)
872
		return false;
873
874
	memset(out, 0, sizeof *out);
875
	out->seconds       = seconds;
876
	out->frames        = n;
877
	out->budget_cycles = budget_cycles;
878
879
	for (int i = 0; i < n; i++)
880
		scratch[i] = perf_frame_work(&ring[newest(i)]);
881
	stat_from_scratch(n, &out->work);
882
883
	for (int i = 0; i < n; i++)
884
		scratch[i] = ring[newest(i)].total;
885
	stat_from_scratch(n, &out->total);
886
887
	for (int i = 0; i < n; i++)
888
		scratch[i] = ring[newest(i)].irq;
889
	stat_from_scratch(n, &out->irq);
890
891
	for (int i = 0; i < n; i++)
892
		scratch[i] = ring[newest(i)].host_us;
893
	stat_from_scratch(n, &out->host_us);
894
895
	// Budget-period work carries the overrun answer, so the scan for the worst
896
	// frame rides along with the copy rather than making a second pass.
897
	int overruns = 0;
898
	const uint32_t threshold = overrun_threshold();
899
	for (int i = 0; i < n; i++) {
900
		const perf_frame_t *f = &ring[newest(i)];
901
		scratch[i]            = f->period_work;
902
		if (f->period_work > threshold) {
903
			overruns++;
904
			const uint32_t over = f->period_work - budget_cycles;
905
			if (over > out->worst_overrun) {
906
				out->worst_overrun = over;
907
				out->worst_frame   = f->frame;
908
			}
909
		}
910
	}
911
	stat_from_scratch(n, &out->budget_work);
912
913
	out->overruns    = overruns;
914
	out->overrun_pct = 100.0f * (float)overruns / (float)n;
915
916
	if (budget_cycles) {
917
		const float b        = (float)budget_cycles;
918
		out->mean_utilization = 100.0f * (float)out->budget_work.mean / b;
919
		out->p95_utilization  = 100.0f * (float)out->budget_work.p95 / b;
920
		out->max_utilization  = 100.0f * (float)out->budget_work.max / b;
921
	}
922
	return true;
923
}
924
925
bool
926
perf_budget_zone_window(int zone, float seconds, perf_stat_t *out)
927
{
928
	if (!out || zone < 0 || zone >= PERF_MAX_ZONES || !zones[zone].used)
929
		return false;
930
931
	memset(out, 0, sizeof *out);
932
933
	// Zones past the ring's per-frame slots have no history to take
934
	// percentiles over. Report the live frame flat rather than nothing, so the
935
	// caller still sees a number and can tell it apart by its equal fields.
936
	if (zone >= PERF_RING_ZONES) {
937
		const uint32_t v = cur_zone[zone];
938
		out->min = out->max = out->mean = out->p50 = out->p95 = out->p99 = v;
939
		return true;
940
	}
941
	if (!ring)
942
		return false;
943
944
	const int n = window_frames(seconds);
945
	if (n <= 0)
946
		return false;
947
	for (int i = 0; i < n; i++)
948
		scratch[i] = ring[newest(i)].zone[zone];
949
	stat_from_scratch(n, out);
950
	return true;
951
}
952
953
bool
954
perf_budget_drain_overruns(perf_overrun_report_t *out)
955
{
956
	if (pending_overruns.overruns == 0) {
957
		pending_overruns.frames = 0;
958
		return false;
959
	}
960
	if (out)
961
		*out = pending_overruns;
962
	memset(&pending_overruns, 0, sizeof pending_overruns);
963
	return true;
964
}