OXYGENHUB
◆ main enhanced-x16-emulator / tests / test_sprite_multiplex.c
14.2 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
// Sprite multiplexing, end to end through the real video.c.
2
//
3
// test_sprite_trace.c drives the recorder directly with synthetic scanlines.
4
// This one drives VERA: it writes sprite attributes into VRAM, steps the beam a
5
// cycle at a time, and rewrites a slot part-way down the frame the way a raster
6
// IRQ handler would. What it checks is that the trace's answer comes from what
7
// render_sprite_line() actually put on screen -- so a sprite whose bitmap is
8
// entirely transparent is *not* counted, even though its attributes are live
9
// and its Y span is on screen.
10
11
#include "support/video_fixture.h"
12
13
// SDL redefines main to SDL_main, which makes a plain int main(void) a C4026
14
// and, under /WX, fatal. Undoing the macro is what lets this test build on
15
// MSVC; several older test files in this tree still trip over it.
16
#ifdef main
17
#undef main
18
#endif
19
20
#include "support/harness.h"
21
22
#include "sprite_trace.h"
23
#include "video.h"
24
25
#include <stdint.h>
26
#include <string.h>
27
28
// video.c exports these; the test needs to know where the beam is.
29
extern uint16_t vga_scan_pos_y;
30
extern int      frame_count;
31
32
#define DC_VIDEO 0x09
33
#define ADDR_SPRDATA 0x1FC00u
34
#define SPRITE_GFX_OPAQUE 0x4000u
35
#define SPRITE_GFX_CLEAR 0x5000u
36
#define SPRITE_GFX_BIG 0x8000u
37
38
#define MHZ 8.0f
39
40
static void
41
step_one_cycle(void)
42
{
43
	video_step(MHZ, 1, false);
44
}
45
46
// Run until the beam reaches `target`, giving up rather than spinning forever
47
// if the video mode is not what the test thinks it is.
48
static bool
49
step_to_line(uint16_t target)
50
{
51
	for (long guard = 0; guard < 2000000; guard++) {
52
		if (vga_scan_pos_y == target) {
53
			return true;
54
		}
55
		step_one_cycle();
56
	}
57
	return false;
58
}
59
60
static bool
61
step_one_frame(void)
62
{
63
	const int start = frame_count;
64
	for (long guard = 0; guard < 2000000; guard++) {
65
		if (frame_count != start) {
66
			return true;
67
		}
68
		step_one_cycle();
69
	}
70
	return false;
71
}
72
73
// wcode/hcode are VERA's size codes: 0=8, 1=16, 2=32, 3=64 pixels.
74
static void
75
write_sprite_sized(int slot, uint32_t addr, int x, int y, int z, int wcode, int hcode, int mode)
76
{
77
	uint8_t a[8];
78
	memset(a, 0, sizeof(a));
79
	a[0] = (uint8_t)((addr >> 5) & 0xff);
80
	a[1] = (uint8_t)(((addr >> 13) & 0x0f) | (mode ? 0x80 : 0));
81
	a[2] = (uint8_t)(x & 0xff);
82
	a[3] = (uint8_t)((x >> 8) & 3);
83
	a[4] = (uint8_t)(y & 0xff);
84
	a[5] = (uint8_t)((y >> 8) & 3);
85
	a[6] = (uint8_t)((z & 3) << 2); // no flip
86
	a[7] = (uint8_t)(((hcode & 3) << 6) | ((wcode & 3) << 4));
87
	for (int b = 0; b < 8; b++) {
88
		video_space_write(ADDR_SPRDATA + (uint32_t)slot * 8 + b, a[b]);
89
	}
90
}
91
92
static void
93
write_sprite(int slot, uint32_t addr, int x, int y, int z)
94
{
95
	write_sprite_sized(slot, addr, x, y, z, 0, 0, 0); // 8x8, 4bpp
96
}
97
98
// An 8x8 4bpp sprite is eight rows of four bytes.
99
static void
100
fill_sprite_gfx(uint32_t addr, uint8_t value)
101
{
102
	for (uint32_t i = 0; i < 8 * 4; i++) {
103
		video_space_write(addr + i, value);
104
	}
105
}
106
107
static void
108
setup(void)
109
{
110
	video_reset();
111
	// VGA out, sprites enabled.
112
	video_write(DC_VIDEO, 0x41);
113
	fill_sprite_gfx(SPRITE_GFX_OPAQUE, 0x11); // both nibbles a visible colour
114
	fill_sprite_gfx(SPRITE_GFX_CLEAR, 0x00);  // wholly transparent
115
	for (uint32_t i = 0; i < 64 * 64; i++) {
116
		video_space_write(SPRITE_GFX_BIG + i, 0x22); // 64x64 8bpp, opaque
117
	}
118
	for (int slot = 0; slot < 128; slot++) {
119
		write_sprite(slot, SPRITE_GFX_OPAQUE, 0, 0, 0); // z=0: disabled
120
	}
121
}
122
123
// One slot, two sprites, a raster rewrite between them: the whole point of the
124
// exercise, measured through the real renderer.
125
static void
126
test_a_slot_rewritten_mid_frame_yields_two_effective_sprites(void)
127
{
128
	setup();
129
	check(sprite_trace_set_enabled(true), "tracing enables");
130
131
	write_sprite(0, SPRITE_GFX_OPAQUE, 100, 10, 1);
132
133
	// Let a frame boundary pass so the trace is recording a clean frame.
134
	check(step_one_frame(), "the beam reaches a frame boundary");
135
136
	// Now the frame under test: the sprite shows at y=10..17, then the handler
137
	// hands the slot to a second sprite that shows at y=200..207.
138
	check(step_to_line(50), "the beam reaches the split");
139
	write_sprite(0, SPRITE_GFX_OPAQUE, 140, 200, 1);
140
	check(step_one_frame(), "the frame under test completes");
141
142
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
143
	check(f != NULL, "the frame is published");
144
	if (!f) {
145
		return;
146
	}
147
148
	check_eq(f->summary.effective_sprites, 2u, "one slot showed two sprites");
149
	check_eq(f->summary.slots_used, 1u, "using one hardware slot");
150
	check_eq(f->summary.slots_multiplexed, 1u, "the slot is reported as multiplexed");
151
	check(f->summary.multiplexing, "multiplexing is detected");
152
	check_eq(f->summary.phantom_writes, 0u, "neither value was wasted");
153
	check_eq(f->summary.late_writes, 0u, "neither write was late");
154
155
	// The residency map has to show the handover at the right place.
156
	const uint16_t at12  = f->line_gen[(size_t)12 * SPRITE_TRACE_SLOTS + 0];
157
	const uint16_t at202 = f->line_gen[(size_t)202 * SPRITE_TRACE_SLOTS + 0];
158
	check(at12 != at202, "the slot holds different values above and below the split");
159
	check(f->line_flags[(size_t)12 * SPRITE_TRACE_SLOTS + 0] & SPRITE_TRACE_DREW,
160
	      "the first sprite drew on its own lines");
161
	check(f->line_flags[(size_t)202 * SPRITE_TRACE_SLOTS + 0] & SPRITE_TRACE_DREW,
162
	      "the second sprite drew on its own lines");
163
	check(!(f->line_flags[(size_t)100 * SPRITE_TRACE_SLOTS + 0] & SPRITE_TRACE_DREW),
164
	      "and neither drew on a line between them");
165
166
	if (at202 < f->gen_count) {
167
		check_eq(f->gens[at202].y, 200u, "the second generation carries its own Y");
168
		check_eq(f->gens[at202].lines_drawn, 8u, "it drew on all eight of its rows");
169
	}
170
}
171
172
// The trace reports what reached the screen, not what the attribute table says
173
// should have. A fully transparent sprite is live, enabled and on screen, and
174
// still contributes nothing.
175
static void
176
test_a_transparent_sprite_is_not_an_effective_sprite(void)
177
{
178
	setup();
179
	sprite_trace_set_enabled(false);
180
	check(sprite_trace_set_enabled(true), "tracing re-enables");
181
182
	write_sprite(1, SPRITE_GFX_OPAQUE, 50, 20, 1); // draws
183
	write_sprite(2, SPRITE_GFX_CLEAR, 60, 20, 1);  // enabled, on screen, invisible
184
185
	check(step_one_frame(), "the beam reaches a frame boundary");
186
	check(step_one_frame(), "a full frame is recorded");
187
188
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
189
	check(f != NULL, "the frame is published");
190
	if (!f) {
191
		return;
192
	}
193
194
	check_eq(f->summary.effective_sprites, 1u, "only the sprite with visible pixels counts");
195
	check_eq(f->summary.slots_used, 1u, "one slot reached the screen");
196
197
	const size_t clear_at20 = (size_t)20 * SPRITE_TRACE_SLOTS + 2;
198
	check(f->line_flags[clear_at20] & SPRITE_TRACE_ONSCREEN,
199
	      "the transparent sprite is still recorded as on screen");
200
	check(!(f->line_flags[clear_at20] & SPRITE_TRACE_DREW),
201
	      "but it is not recorded as having drawn");
202
}
203
204
// The per-scanline VERA budget has to be charged from the renderer's own
205
// accounting, and only on the lines that actually did sprite work.
206
static void
207
test_the_budget_is_charged_on_the_lines_that_did_the_work(void)
208
{
209
	setup();
210
	sprite_trace_set_enabled(false);
211
	check(sprite_trace_set_enabled(true), "tracing re-enables");
212
213
	write_sprite(0, SPRITE_GFX_OPAQUE, 100, 30, 1);
214
215
	check(step_one_frame(), "the beam reaches a frame boundary");
216
	check(step_one_frame(), "a full frame is recorded");
217
218
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
219
	check(f != NULL, "the frame is published");
220
	if (!f) {
221
		return;
222
	}
223
224
	// Every line pays one cycle per slot looked at; a line carrying the sprite
225
	// pays for its fetches and pixels on top.
226
	const uint16_t idle = f->lines[10].budget_used;
227
	const uint16_t busy = f->lines[32].budget_used;
228
	check(busy > idle, "a line with a sprite on it costs more than one without");
229
	check(idle >= 128, "every line still pays to look at all 128 slots");
230
	check_eq(f->lines[32].drawn, 1u, "one sprite drew on that line");
231
	check_eq(f->summary.lines_exhausted, 0u, "a single small sprite is nowhere near the budget");
232
	check(f->summary.peak_budget < SPRITE_TRACE_LINE_BUDGET,
233
	      "peak demand stays under the hardware ceiling");
234
}
235
236
// Sprites are never free. Once a scanline runs out of render time, VERA stops
237
// both its sprite search and its line renderer until the next line
238
// (sprite_renderer.v:171,349), so everything after the cut is off screen. This
239
// is the check that the emulator drops them instead of drawing them for
240
// nothing.
241
static void
242
test_a_line_out_of_render_time_drops_the_rest(void)
243
{
244
	setup();
245
	sprite_trace_set_enabled(false);
246
	check(sprite_trace_set_enabled(true), "tracing re-enables");
247
248
	// 64x64 8bpp sprites cost roughly 64 pixels + 16 fetches + 3 dispatch per
249
	// line each, so a dozen of them on one line cannot fit in 798 clocks.
250
	const int overload = 24;
251
	for (int slot = 0; slot < overload; slot++) {
252
		write_sprite_sized(slot, SPRITE_GFX_BIG, slot * 8, 100, 1, 3, 3, 1);
253
	}
254
255
	check(step_one_frame(), "the beam reaches a frame boundary");
256
	check(step_one_frame(), "a full frame is recorded");
257
258
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
259
	check(f != NULL, "the frame is published");
260
	if (!f) {
261
		return;
262
	}
263
264
	const sprite_line_stat_t *line = &f->lines[120]; // inside the 100..163 band
265
	check(line->exhausted, "the overloaded line runs out of render time");
266
	check(line->demand > SPRITE_TRACE_LINE_BUDGET,
267
	      "and it wanted more than the line has to give");
268
	check_eq(line->budget_used, (uint32_t)SPRITE_TRACE_LINE_BUDGET,
269
	         "it spends exactly the ceiling, never more");
270
	check(line->dropped > 0, "sprites were dropped");
271
	check(line->drawn < overload, "not every sprite reached the screen");
272
	check(f->summary.sprites_dropped > 0, "the frame reports the drops");
273
274
	// The cut is ordered: low slots win, high slots lose. That is the whole
275
	// reason a multiplexer cares which slot a sprite goes in.
276
	check(line->cut_slot != SPRITE_TRACE_NO_LINE, "the cut point is recorded");
277
	check(f->line_flags[(size_t)120 * SPRITE_TRACE_SLOTS + 0] & SPRITE_TRACE_DREW,
278
	      "slot 0 drew: the search reaches it first");
279
	check(!(f->line_flags[(size_t)120 * SPRITE_TRACE_SLOTS + (overload - 1)] & SPRITE_TRACE_DREW),
280
	      "the last slot did not: time had run out before the search got there");
281
282
	// A line the sprites do not cover must be untouched by all of this.
283
	check(!f->lines[300].exhausted, "a line without sprites has time to spare");
284
}
285
286
// The counterpart to the test above: stepping over empty slots must not starve
287
// the line. VERA's search FSM advances one slot per clock whether or not the
288
// line renderer is busy (sprite_renderer.v:146-160), so scanning empties
289
// overlaps with drawing. Charging them serially would drop sprites real
290
// hardware draws -- a false dropout, which is worse than none at all.
291
//
292
// Measured as a difference rather than an absolute: the same eight sprites are
293
// placed low in the slot table, where the 120 empty slots are scanned after
294
// them and overlap with drawing, and then high, where the empties are scanned
295
// first and cannot overlap. If the search overlapped, the first costs less.
296
static void
297
test_stepping_over_empty_slots_overlaps_with_drawing(void)
298
{
299
	setup();
300
	sprite_trace_set_enabled(false);
301
	check(sprite_trace_set_enabled(true), "tracing re-enables");
302
303
	for (int n = 0; n < 8; n++) {
304
		write_sprite_sized(n, SPRITE_GFX_BIG, n * 70, 100, 1, 3, 3, 1);
305
	}
306
	check(step_one_frame(), "the beam reaches a frame boundary");
307
	check(step_one_frame(), "a full frame is recorded");
308
309
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
310
	check(f != NULL, "the low-slot frame is published");
311
	if (!f) {
312
		return;
313
	}
314
	const uint32_t low_demand = f->lines[120].demand;
315
	const uint16_t low_drawn  = f->lines[120].drawn;
316
	check(!f->lines[120].exhausted, "eight big sprites fit on a line");
317
	check_eq(low_drawn, 8u, "all eight drew");
318
319
	// Same eight sprites, now at the top of the slot table.
320
	setup();
321
	sprite_trace_set_enabled(false);
322
	check(sprite_trace_set_enabled(true), "tracing re-enables");
323
	for (int n = 0; n < 8; n++) {
324
		write_sprite_sized(120 + n, SPRITE_GFX_BIG, n * 70, 100, 1, 3, 3, 1);
325
	}
326
	check(step_one_frame(), "the beam reaches a frame boundary");
327
	check(step_one_frame(), "a full frame is recorded");
328
329
	f = sprite_trace_last_frame();
330
	check(f != NULL, "the high-slot frame is published");
331
	if (!f) {
332
		return;
333
	}
334
	const uint32_t high_demand = f->lines[120].demand;
335
	check_eq(f->lines[120].drawn, 8u, "all eight still drew");
336
	check(high_demand > low_demand,
337
	      "the same sprites cost more when no drawing overlaps the search");
338
}
339
340
// How many sprites actually fit on a line, which is the number a multiplexer
341
// budgets against. Pinned as a test because it falls out of the timing model:
342
// if the per-sprite cost drifts, this moves and someone should know why.
343
//
344
// Cost per sprite is 2 clocks to dispatch (SF_FIND_SPRITE, SF_START_RENDER),
345
// 1 for STATE_IDLE, then 1 per pixel and 1 per 32-bit fetch -- 8 pixels per
346
// fetch at 4bpp, 4 at 8bpp. An 8x8 4bpp sprite is therefore 3 + 8 + 1 = 12
347
// clocks, and 798 / 12 is 66.
348
static void
349
test_how_many_sprites_fit_on_one_line(void)
350
{
351
	setup();
352
	sprite_trace_set_enabled(false);
353
	check(sprite_trace_set_enabled(true), "tracing re-enables");
354
355
	// Every slot enabled and on the same line, spread across the screen.
356
	for (int slot = 0; slot < 128; slot++) {
357
		write_sprite_sized(slot, SPRITE_GFX_OPAQUE, (slot * 5) % 632, 100, 1, 0, 0, 0);
358
	}
359
360
	check(step_one_frame(), "the beam reaches a frame boundary");
361
	check(step_one_frame(), "a full frame is recorded");
362
363
	const sprite_trace_frame_t *f = sprite_trace_last_frame();
364
	check(f != NULL, "the frame is published");
365
	if (!f) {
366
		return;
367
	}
368
369
	const sprite_line_stat_t *line = &f->lines[100];
370
	printf("      8x8 4bpp sprites drawn on one line: %u of 128 (wanted %u of %d clocks)\n",
371
	       line->drawn, line->demand, SPRITE_TRACE_LINE_BUDGET);
372
373
	check(line->exhausted, "128 sprites on one line does not fit");
374
	check(line->drawn >= 60 && line->drawn <= 70,
375
	      "about 66 of them fit, at 12 clocks each");
376
	check_eq((uint32_t)(line->drawn + line->dropped), 128u,
377
	         "every slot is accounted for as drawn or dropped");
378
}
379
380
int
381
main(void)
382
{
383
	test_a_slot_rewritten_mid_frame_yields_two_effective_sprites();
384
	test_a_transparent_sprite_is_not_an_effective_sprite();
385
	test_the_budget_is_charged_on_the_lines_that_did_the_work();
386
	test_a_line_out_of_render_time_drops_the_rest();
387
	test_stepping_over_empty_slots_overlaps_with_drawing();
388
	test_how_many_sprites_fit_on_one_line();
389
	sprite_trace_set_enabled(false);
390
	return x16_test_summary("sprite_multiplex");
391
}