OXYGENHUB
◆ main enhanced-x16-emulator / tests / test_sprite_trace.c
13.6 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
// Sprite multiplexing trace: generation lifetimes, the effective/phantom/late
2
// classification, VERA budget accounting and burst costing.
3
//
4
// sprite_trace has no dependency on video.c -- it is fed scalars -- so these
5
// drive it directly rather than standing a VERA up around it. Each test builds
6
// a frame the way render_sprite_line() would: advance to open the frame, then
7
// line_begin / note_slot / line_end per scanline, then advance again to close
8
// and publish it.
9
10
#include "../src/sprite_trace.h"
11
#include "support/harness.h"
12
13
#include <stdio.h>
14
#include <string.h>
15
16
#define W8 0
17
#define W16 1
18
#define W32 2
19
#define W64 3
20
21
static uint8_t attrs[SPRITE_TRACE_SLOTS][8];
22
23
static void
24
set_attr(int slot, uint32_t addr, int x, int y, int wcode, int hcode, int z, int pal)
25
{
26
	uint8_t *a = attrs[slot];
27
	a[0]       = (uint8_t)((addr >> 5) & 0xff);
28
	a[1]       = (uint8_t)((addr >> 13) & 0x0f);
29
	a[2]       = (uint8_t)(x & 0xff);
30
	a[3]       = (uint8_t)((x >> 8) & 3);
31
	a[4]       = (uint8_t)(y & 0xff);
32
	a[5]       = (uint8_t)((y >> 8) & 3);
33
	a[6]       = (uint8_t)((z & 3) << 2);
34
	a[7]       = (uint8_t)(((hcode & 3) << 6) | ((wcode & 3) << 4) | (pal & 0x0f));
35
}
36
37
static void
38
clear_attrs(void)
39
{
40
	memset(attrs, 0, sizeof(attrs));
41
}
42
43
// Rewrite a slot's attributes the way a raster handler would: eight stores,
44
// each reported to the trace with the value it replaced.
45
static void
46
write_slot(int slot, uint16_t line, uint32_t cycle, uint32_t addr, int x, int y, int wcode,
47
           int hcode, int z, int pal)
48
{
49
	uint8_t old[8];
50
	memcpy(old, attrs[slot], 8);
51
	set_attr(slot, addr, x, y, wcode, hcode, z, pal);
52
	for (int b = 0; b < 8; b++) {
53
		sprite_trace_note_write((uint8_t)slot, (uint8_t)b, old[b], attrs[slot][b], line,
54
		                        cycle + (uint32_t)b * 5);
55
	}
56
}
57
58
// One scanline of rendering. `drawing_slot` is the slot that put pixels down on
59
// this line, or -1 for none. Every slot is reported as evaluated, which is what
60
// the real renderer does until its line timer runs out.
61
static void
62
end_line(uint16_t line, uint32_t demand, uint16_t drawn, uint16_t dropped)
63
{
64
	sprite_line_stat_t st;
65
	memset(&st, 0, sizeof(st));
66
	const bool over = demand > SPRITE_TRACE_LINE_BUDGET;
67
	st.budget_used  = (uint16_t)(over ? SPRITE_TRACE_LINE_BUDGET : demand);
68
	st.demand       = demand;
69
	st.evaluated    = SPRITE_TRACE_SLOTS;
70
	st.drawn        = drawn;
71
	st.dropped      = dropped;
72
	st.cut_slot     = over ? 0 : SPRITE_TRACE_NO_LINE;
73
	st.exhausted    = over;
74
	sprite_trace_line_end(line, &st);
75
}
76
77
static void
78
render_line_with(uint16_t line, uint32_t cycle, int drawing_slot, uint16_t budget)
79
{
80
	sprite_trace_line_begin(line, cycle);
81
	uint16_t drawn = 0;
82
	for (int slot = 0; slot < SPRITE_TRACE_SLOTS; slot++) {
83
		uint8_t flags = SPRITE_TRACE_EVALUATED;
84
		if (slot == drawing_slot) {
85
			flags |= SPRITE_TRACE_ONSCREEN | SPRITE_TRACE_DREW;
86
			drawn++;
87
		}
88
		sprite_trace_note_slot(line, (uint8_t)slot, attrs[slot],
89
		                       (uint16_t)(slot == drawing_slot ? budget : 1), flags);
90
	}
91
	end_line(line, budget, drawn, 0);
92
}
93
94
static void
95
test_disabled_records_nothing(void)
96
{
97
	sprite_trace_set_enabled(false);
98
	check(sprite_trace_last_frame() == NULL, "no frame while tracing is off");
99
	check(sprite_trace_memory_usage() == 0, "no memory held while tracing is off");
100
}
101
102
static void
103
test_a_static_sprite_is_one_effective_sprite(void)
104
{
105
	clear_attrs();
106
	check(sprite_trace_set_enabled(true), "tracing enables");
107
108
	set_attr(0, 0x4000, 100, 10, W16, W16, 1, 0);
109
	sprite_trace_frame_advance(1, 0, attrs);
110
	for (uint16_t y = 0; y < 40; y++) {
111
		render_line_with(y, y * 100u, (y >= 10 && y < 26) ? 0 : -1, 40);
112
	}
113
	sprite_trace_frame_advance(2, 4000, attrs);
114
115
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
116
	check(f != NULL, "a completed frame is published");
117
	if (!f) {
118
		return;
119
	}
120
	check_eq(f->summary.frame, 1u, "the published frame is the one that just ended");
121
	check_eq(f->summary.effective_sprites, 1u, "one sprite drew");
122
	check_eq(f->summary.slots_used, 1u, "one slot drew");
123
	check_eq(f->summary.slots_multiplexed, 0u, "nothing was multiplexed");
124
	check_eq(f->summary.phantom_writes, 0u, "no writes were wasted");
125
	check(!f->summary.multiplexing, "multiplexing is not reported");
126
127
	// The 127 unused slots are carried, never written, and must not be counted
128
	// as wasted work.
129
	uint16_t drew = 0;
130
	for (uint16_t i = 0; i < f->gen_count; i++) {
131
		if (f->gens[i].flags & SPRITE_GEN_DREW) {
132
			drew++;
133
			check_eq(f->gens[i].lines_drawn, 16u, "it drew on all 16 of its lines");
134
			check_eq(f->gens[i].first_line, 10u, "first line matches its Y");
135
			check_eq(f->gens[i].last_line, 25u, "last line matches its Y plus height");
136
		}
137
	}
138
	check_eq(drew, 1u, "exactly one generation drew");
139
}
140
141
static void
142
test_one_slot_reused_counts_as_two_sprites(void)
143
{
144
	clear_attrs();
145
	sprite_trace_set_enabled(false);
146
	check(sprite_trace_set_enabled(true), "tracing re-enables");
147
148
	// Slot 0 carries sprite A over the top of the frame, then the handler hands
149
	// the slot to sprite B half way down -- 128 hardware slots, 2 sprites.
150
	set_attr(0, 0x4000, 10, 0, W64, W64, 1, 0);
151
	sprite_trace_frame_advance(1, 0, attrs);
152
153
	for (uint16_t y = 0; y < 64; y++) {
154
		render_line_with(y, y * 100u, 0, 200);
155
	}
156
	write_slot(0, 64, 6400, 0x8000, 20, 64, W64, W64, 1, 0);
157
	for (uint16_t y = 64; y < 128; y++) {
158
		render_line_with(y, y * 100u, 0, 200);
159
	}
160
	sprite_trace_frame_advance(2, 12800, attrs);
161
162
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
163
	check(f != NULL, "the multiplexed frame is published");
164
	if (!f) {
165
		return;
166
	}
167
	check_eq(f->summary.effective_sprites, 2u, "one slot showed two sprites");
168
	check_eq(f->summary.slots_used, 1u, "still only one hardware slot");
169
	check_eq(f->summary.slots_multiplexed, 1u, "that slot is reported as multiplexed");
170
	check_eq(f->summary.max_gens_in_slot, 2u, "two values were held");
171
	check(f->summary.multiplexing, "multiplexing is reported");
172
	check_eq(f->summary.phantom_writes, 0u, "both values were displayed");
173
	check_eq(f->summary.late_writes, 0u, "neither write was late");
174
175
	// The residency map must show the handover, not one sprite over the whole
176
	// frame: that separation is the entire point of the trace.
177
	uint16_t top    = f->line_gen[(size_t)32 * SPRITE_TRACE_SLOTS + 0];
178
	uint16_t bottom = f->line_gen[(size_t)96 * SPRITE_TRACE_SLOTS + 0];
179
	check(top != bottom, "the slot maps to different values above and below the split");
180
	if (top < f->gen_count && bottom < f->gen_count) {
181
		check_eq(f->gens[top].address, 0x4000u, "the top band is sprite A");
182
		check_eq(f->gens[bottom].address, 0x8000u, "the bottom band is sprite B");
183
		check_eq(f->gens[bottom].index_in_slot, 1u, "sprite B is the slot's second value");
184
		// The handler stored all eight bytes, but only the address high bits,
185
		// the X low byte and the Y low byte differ between the two sprites.
186
		// Separating the two is what makes redundant stores visible.
187
		check_eq(f->gens[bottom].writes, 3u, "three of the eight stores changed anything");
188
		check_eq(f->summary.mutations, 8u, "all eight were still paid for");
189
		check_eq(f->gens[bottom].write_line, 64u, "it was stamped with the line it completed on");
190
	}
191
}
192
193
static void
194
test_a_write_that_never_draws_is_phantom(void)
195
{
196
	clear_attrs();
197
	sprite_trace_set_enabled(false);
198
	check(sprite_trace_set_enabled(true), "tracing re-enables");
199
200
	sprite_trace_frame_advance(1, 0, attrs);
201
	for (uint16_t y = 0; y < 20; y++) {
202
		render_line_with(y, y * 100u, -1, 20);
203
	}
204
	// Written, then the beam never draws it: a slot update that achieved
205
	// nothing, which reading the attribute table cannot reveal.
206
	write_slot(3, 20, 2000, 0x6000, 40, 400, W16, W16, 1, 0);
207
	for (uint16_t y = 20; y < 40; y++) {
208
		render_line_with(y, y * 100u, -1, 20);
209
	}
210
	sprite_trace_frame_advance(2, 4000, attrs);
211
212
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
213
	check(f != NULL, "the frame is published");
214
	if (!f) {
215
		return;
216
	}
217
	check_eq(f->summary.effective_sprites, 0u, "nothing drew");
218
	check_eq(f->summary.phantom_writes, 1u, "the wasted write is counted once");
219
	check_eq(f->summary.mutations, 8u, "all eight stores were seen");
220
}
221
222
static void
223
test_a_write_after_its_own_y_span_is_late(void)
224
{
225
	clear_attrs();
226
	sprite_trace_set_enabled(false);
227
	check(sprite_trace_set_enabled(true), "tracing re-enables");
228
229
	sprite_trace_frame_advance(1, 0, attrs);
230
	for (uint16_t y = 0; y < 300; y++) {
231
		render_line_with(y, y * 100u, -1, 20);
232
	}
233
	// The sprite is meant to appear at y=10..25, but the handler only gets
234
	// round to storing it at line 300 -- the classic multiplexer bug.
235
	write_slot(5, 300, 30000, 0x6000, 40, 10, W16, W16, 1, 0);
236
	for (uint16_t y = 300; y < 320; y++) {
237
		render_line_with(y, y * 100u, -1, 20);
238
	}
239
	sprite_trace_frame_advance(2, 32000, attrs);
240
241
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
242
	check(f != NULL, "the frame is published");
243
	if (!f) {
244
		return;
245
	}
246
	check_eq(f->summary.late_writes, 1u, "the late write is identified");
247
248
	bool flagged = false;
249
	for (uint16_t i = 0; i < f->gen_count; i++) {
250
		if (f->gens[i].slot == 5 && (f->gens[i].flags & SPRITE_GEN_LATE)) {
251
			flagged = true;
252
		}
253
	}
254
	check(flagged, "the generation itself carries the late flag");
255
}
256
257
static void
258
test_budget_overrun_is_reported(void)
259
{
260
	clear_attrs();
261
	sprite_trace_set_enabled(false);
262
	check(sprite_trace_set_enabled(true), "tracing re-enables");
263
264
	sprite_trace_frame_advance(1, 0, attrs);
265
	for (uint16_t y = 0; y < 10; y++) {
266
		// Line 4 demands more sprite work than VERA can fetch in a scanline.
267
		render_line_with(y, y * 100u, 0, (uint16_t)(y == 4 ? 1500 : 300));
268
	}
269
	sprite_trace_frame_advance(2, 1000, attrs);
270
271
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
272
	check(f != NULL, "the frame is published");
273
	if (!f) {
274
		return;
275
	}
276
	check_eq(f->summary.lines_exhausted, 1u, "one scanline ran out of render time");
277
	check_eq(f->summary.peak_line, 4u, "the peak is the overloaded line");
278
	check_eq(f->summary.peak_demand, 1500u, "the peak records what the line wanted");
279
	check_eq(f->summary.peak_budget, (uint32_t)SPRITE_TRACE_LINE_BUDGET,
280
	         "but only the ceiling was actually spent");
281
	check(f->lines[4].exhausted, "the line is marked exhausted");
282
	check(!f->lines[3].exhausted, "a line within budget is not");
283
}
284
285
static void
286
test_writes_are_costed_in_bursts(void)
287
{
288
	clear_attrs();
289
	sprite_trace_set_enabled(false);
290
	check(sprite_trace_set_enabled(true), "tracing re-enables");
291
	sprite_trace_set_burst_gap(128);
292
293
	sprite_trace_frame_advance(1, 0, attrs);
294
	sprite_trace_line_begin(0, 0);
295
	// Two visits to the raster handler, 40 cycles apart internally and 4000
296
	// cycles apart from each other.
297
	for (uint32_t i = 0; i < 5; i++) {
298
		sprite_trace_note_write(0, (uint8_t)i, 0, (uint8_t)(i + 1), 0, 1000 + i * 10);
299
	}
300
	for (uint32_t i = 0; i < 5; i++) {
301
		sprite_trace_note_write(1, (uint8_t)i, 0, (uint8_t)(i + 1), 0, 5000 + i * 10);
302
	}
303
	end_line(0, 10, 0, 0);
304
	sprite_trace_frame_advance(2, 10000, attrs);
305
306
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
307
	check(f != NULL, "the frame is published");
308
	if (!f) {
309
		return;
310
	}
311
	check_eq(f->summary.mutations, 10u, "every store is counted");
312
	check_eq(f->summary.mutation_bursts, 2u, "the stores fall into two bursts");
313
	check_eq(f->summary.mutation_cycles, 80u, "each burst spans 40 cycles");
314
	check_eq(f->summary.burst_gap, 128u, "the gap the costing used is reported");
315
316
	// A wider gap merges them into one burst spanning the whole gap.
317
	sprite_trace_set_burst_gap(8192);
318
	sprite_trace_frame_advance(3, 10000, attrs);
319
	sprite_trace_line_begin(0, 10000);
320
	for (uint32_t i = 0; i < 5; i++) {
321
		sprite_trace_note_write(0, (uint8_t)i, 0, (uint8_t)(i + 1), 0, 11000 + i * 10);
322
	}
323
	for (uint32_t i = 0; i < 5; i++) {
324
		sprite_trace_note_write(1, (uint8_t)i, 0, (uint8_t)(i + 1), 0, 15000 + i * 10);
325
	}
326
	end_line(0, 10, 0, 0);
327
	sprite_trace_frame_advance(4, 20000, attrs);
328
329
	f = sprite_trace_last_frame();
330
	if (f) {
331
		check_eq(f->summary.mutation_bursts, 1u, "a wide gap merges the two visits");
332
		check_eq(f->summary.mutation_cycles, 4040u, "the merged burst spans both");
333
	}
334
	sprite_trace_set_burst_gap(128);
335
}
336
337
static void
338
test_a_redundant_store_costs_but_does_not_split(void)
339
{
340
	clear_attrs();
341
	sprite_trace_set_enabled(false);
342
	check(sprite_trace_set_enabled(true), "tracing re-enables");
343
344
	set_attr(0, 0x4000, 10, 0, W16, W16, 1, 0);
345
	sprite_trace_frame_advance(1, 0, attrs);
346
	render_line_with(0, 0, 0, 40);
347
348
	// Storing the value that is already there is real bus traffic and real
349
	// cycles, but it changes nothing the beam will draw.
350
	for (int b = 0; b < 8; b++) {
351
		sprite_trace_note_write(0, (uint8_t)b, attrs[0][b], attrs[0][b], 1, 100);
352
	}
353
	for (uint16_t y = 1; y < 16; y++) {
354
		render_line_with(y, y * 100u, 0, 40);
355
	}
356
	sprite_trace_frame_advance(2, 1600, attrs);
357
358
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
359
	check(f != NULL, "the frame is published");
360
	if (!f) {
361
		return;
362
	}
363
	check_eq(f->summary.mutations, 8u, "the redundant stores are still counted");
364
	check_eq(f->summary.effective_sprites, 1u, "but no second generation was opened");
365
	check_eq(f->summary.max_gens_in_slot, 1u, "the slot held one value throughout");
366
}
367
368
static void
369
test_disable_releases_the_buffers(void)
370
{
371
	check(sprite_trace_set_enabled(true), "tracing enables");
372
	check(sprite_trace_memory_usage() > 0, "buffers are held while tracing");
373
	sprite_trace_set_enabled(false);
374
	check_eq((uint32_t)sprite_trace_memory_usage(), 0u, "buffers are released when it stops");
375
	check(sprite_trace_last_frame() == NULL, "no frame is served once disabled");
376
}
377
378
int
379
main(void)
380
{
381
	test_disabled_records_nothing();
382
	test_a_static_sprite_is_one_effective_sprite();
383
	test_one_slot_reused_counts_as_two_sprites();
384
	test_a_write_that_never_draws_is_phantom();
385
	test_a_write_after_its_own_y_span_is_late();
386
	test_budget_overrun_is_reported();
387
	test_writes_are_costed_in_bursts();
388
	test_a_redundant_store_costs_but_does_not_split();
389
	test_disable_releases_the_buffers();
390
	return x16_test_summary("sprite_trace");
391
}