OXYGENHUB
◆ main enhanced-x16-emulator / src / vera_bandwidth.c
11.6 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
// Commander X16 Emulator — VERA VRAM bandwidth accounting.
2
//
3
// See vera_bandwidth.h for the model, the RTL it is quoted from, and -- most
4
// importantly -- the list of places it is knowingly wrong.
5
//
6
// No emulator globals, and no <math.h>: see the header for why both matter.
7
8
#include "vera_bandwidth.h"
9
10
#include <stdlib.h>
11
#include <string.h>
12
13
bool vera_bandwidth_active = false;
14
15
// Ownership, mirroring perf_budget.c.
16
static bool owners[VERA_BW_OWNER_COUNT];
17
18
static uint16_t scanlines_per_frame = 525;
19
20
// Two buffers: the frame being drawn, and the last one finished. Small enough
21
// (about 6 KB each) to be static, unlike sprite_trace's residency maps -- there
22
// is nothing to allocate and so nothing that can fail to allocate.
23
static vera_bw_line_t cur_lines[VERA_BW_LINES];
24
static vera_bw_line_t pub_lines[VERA_BW_LINES];
25
26
static vera_bw_frame_t cur_frame;
27
static vera_bw_frame_t pub_frame;
28
static bool            have_frame = false;
29
30
// ─── The model ──────────────────────────────────────────────────────────────
31
32
// layer_renderer.v:42-51. color_depth 0..3 means 1, 2, 4 and 8 bpp, and
33
// mode = {bitmap_mode, color_depth} (layer_renderer.v:39).
34
uint8_t
35
vera_bandwidth_pixels_per_word(bool bitmap_mode, uint8_t color_depth, uint8_t tile_width)
36
{
37
	const uint8_t mode = (uint8_t)((bitmap_mode ? 4 : 0) | (color_depth & 3));
38
	switch (mode) {
39
		case 0: return tile_width ? 16 : 8; // 1bpp tile
40
		case 1: return tile_width ? 16 : 8; // 2bpp tile
41
		case 2: return 8;                   // 4bpp tile
42
		case 3: return 4;                   // 8bpp tile
43
		case 4: return 32;                  // 1bpp bitmap
44
		case 5: return 16;                  // 2bpp bitmap
45
		case 6: return 8;                   // 4bpp bitmap
46
		default: return 4;                  // 8bpp bitmap
47
	}
48
}
49
50
// layer_renderer.v:68-77. Words of pixel data one tile row costs. Bitmap modes
51
// report 1 because they step a word at a time with no tile structure at all.
52
uint8_t
53
vera_bandwidth_words_per_tile_line(bool bitmap_mode, uint8_t color_depth, uint8_t tile_width)
54
{
55
	const uint8_t mode = (uint8_t)((bitmap_mode ? 4 : 0) | (color_depth & 3));
56
	switch (mode) {
57
		case 0: return 1;                  // 1bpp tile
58
		case 1: return 1;                  // 2bpp tile
59
		case 2: return tile_width ? 2 : 1; // 4bpp tile
60
		case 3: return tile_width ? 4 : 2; // 8bpp tile
61
		default: return 1;                 // every bitmap mode
62
	}
63
}
64
65
// Bus clocks a sprite's fetches occupy. See the header for why this conversion
66
// has to exist: sprite_trace measures render time, this module measures bus
67
// time, and the two are not the same clock.
68
//
69
// One 32-bit word per 8 pixels at 4 bpp, per 4 at 8 bpp -- the emulator's own
70
// sprite loop uses the same divisor as vram_fetch_mask in render_sprite_line().
71
// Rounded up, because a sprite whose width is not a whole number of words still
72
// fetches the last partial one.
73
uint16_t
74
vera_bandwidth_sprite_bus_clocks(uint16_t sprite_width, uint8_t color_mode)
75
{
76
	const uint16_t per_word = (uint16_t)(color_mode ? 4 : 8);
77
	const uint16_t words    = (uint16_t)((sprite_width + per_word - 1) / per_word);
78
	return (uint16_t)(words * VERA_BW_ACCESS_CLOCKS);
79
}
80
81
uint16_t
82
vera_bandwidth_layer_line_fetches(const vera_bw_layer_t *layer)
83
{
84
	if (layer == NULL || !layer->enabled)
85
		return 0;
86
87
	const uint8_t depth = (uint8_t)(layer->color_depth & 3);
88
	const uint8_t tw    = layer->tile_width ? 1 : 0;
89
90
	if (layer->bitmap_mode) {
91
		// No map, and -- the part that surprises people -- no dependence on the
92
		// 320/640 bitmap width either. layer_renderer.v has NO scale input (see
93
		// its port list at :3-33); DC_HSCALE is applied by the composer when it
94
		// reads the line buffer. The renderer just fills the buffer until
95
		// line_done at 640 pixels (layer_renderer.v:488), so a 320-wide bitmap
96
		// fetches exactly as many words as a 640-wide one and simply runs off
97
		// the end of its own line. Halving the width buys VRAM, not bus time.
98
		const uint8_t ppw = vera_bandwidth_pixels_per_word(true, depth, tw);
99
		return (uint16_t)(VERA_BW_LAYER_PIXELS / ppw);
100
	}
101
102
	// Tile modes. The renderer starts at lb_wridx_start = 0 - subtile_hscroll
103
	// (layer_renderer.v:499-501) and stops at 640, so a layer scrolled off a
104
	// tile boundary renders up to one tile more than the screen strictly needs.
105
	const uint8_t  tile_px = (uint8_t)(tw ? 16 : 8);
106
	const uint16_t tiles   = (uint16_t)((VERA_BW_LAYER_PIXELS / tile_px) + (layer->scrolled ? 1 : 0));
107
108
	// A 32-bit word holds two 16-bit map entries (layer_renderer.v:113), and
109
	// the state machine only returns to FETCH_MAP on every other tile
110
	// (layer_renderer.v:305). Which tile is "every other" depends on the
111
	// scrolled tile counter's parity, so this can be one out at the margin --
112
	// limit 3 in the header.
113
	const uint16_t map_fetches  = (uint16_t)((tiles + 1) / 2);
114
	const uint16_t data_fetches = (uint16_t)(tiles * vera_bandwidth_words_per_tile_line(false, depth, tw));
115
116
	return (uint16_t)(map_fetches + data_fetches);
117
}
118
119
// ─── Recording ──────────────────────────────────────────────────────────────
120
121
void
122
vera_bandwidth_line_(uint16_t line, const vera_bw_layer_t *l0, const vera_bw_layer_t *l1)
123
{
124
	if (line >= VERA_BW_LINES)
125
		return;
126
127
	vera_bw_line_t *s = &cur_lines[line];
128
129
	// REPLACE, never accumulate. A mid-line register split makes video.c render
130
	// the same scanline more than once and the last pass is what stays on
131
	// screen, so the last pass is what this records -- the same rule
132
	// sprite_trace_line_begin() follows. Port accesses are left alone: those
133
	// are CPU events that really happened, not a rendering pass that got
134
	// superseded.
135
	s->layer_fetches[0] = vera_bandwidth_layer_line_fetches(l0);
136
	s->layer_fetches[1] = vera_bandwidth_layer_line_fetches(l1);
137
	s->rendered         = true;
138
}
139
140
void
141
vera_bandwidth_note_port_(bool write, uint8_t vram_bytes, uint16_t line)
142
{
143
	if (write)
144
		cur_frame.port_writes++;
145
	else
146
		cur_frame.port_reads++;
147
148
	cur_frame.port_accesses++;
149
	cur_frame.port_vram_bytes += vram_bytes;
150
151
	if (line < VERA_BW_LINES)
152
		cur_lines[line].port_accesses++;
153
}
154
155
void
156
vera_bandwidth_note_fx_cache_write_(uint16_t line)
157
{
158
	(void)line;
159
	cur_frame.fx_cache_writes++;
160
}
161
162
void
163
vera_bandwidth_note_fx_affine_(uint16_t line)
164
{
165
	// The affine prefetch reads the tile map and then the tile, so it costs one
166
	// access beyond an ordinary port read (video.c fx_affine_prefetch()).
167
	cur_frame.fx_affine_fetches++;
168
	cur_frame.port_accesses++;
169
	if (line < VERA_BW_LINES)
170
		cur_lines[line].port_accesses++;
171
}
172
173
// ─── Frame boundary ─────────────────────────────────────────────────────────
174
175
static int
176
cmp_u16(const void *a, const void *b)
177
{
178
	const uint16_t x = *(const uint16_t *)a;
179
	const uint16_t y = *(const uint16_t *)b;
180
	return (x > y) - (x < y);
181
}
182
183
// Derive every frame-level figure from the line array in one pass, rather than
184
// keeping running totals. A line can be rendered more than once, and running
185
// totals would count those lines twice; a single pass over the finished array
186
// cannot. sprite_trace_frame_end() derives its per-generation totals the same
187
// way, for the same reason.
188
static void
189
summarise(vera_bw_frame_t *f, const vera_bw_line_t *lines)
190
{
191
	static uint16_t sorted[VERA_BW_LINES];
192
	uint16_t        n = 0;
193
194
	f->layer_fetches[0] = f->layer_fetches[1] = 0;
195
	f->layer_peak_line[0] = f->layer_peak_line[1] = 0;
196
	f->layer_peak_fetches[0] = f->layer_peak_fetches[1] = 0;
197
	f->peak_line = f->peak_clocks = 0;
198
	f->total_clocks = f->total_fetches = 0;
199
	f->lines_over = 0;
200
201
	for (uint16_t i = 0; i < VERA_BW_LINES; i++) {
202
		const vera_bw_line_t *s = &lines[i];
203
		// A line counts if anything touched the bus on it. Testing `rendered`
204
		// alone would drop CPU port traffic on a line no layer drew -- a
205
		// program that switches the layers off and pushes VRAM through $9F23
206
		// would then report as using no bandwidth at all.
207
		if (!s->rendered && s->port_accesses == 0)
208
			continue;
209
210
		const uint32_t fetches = (uint32_t)s->layer_fetches[0] + s->layer_fetches[1] + s->port_accesses;
211
		const uint32_t clocks  = vera_bw_line_clocks(s);
212
213
		for (int l = 0; l < 2; l++) {
214
			f->layer_fetches[l] += s->layer_fetches[l];
215
			if (s->layer_fetches[l] > f->layer_peak_fetches[l]) {
216
				f->layer_peak_fetches[l] = s->layer_fetches[l];
217
				f->layer_peak_line[l]    = i;
218
			}
219
		}
220
221
		f->total_fetches += fetches;
222
		f->total_clocks  += clocks;
223
224
		if (clocks > f->peak_clocks) {
225
			f->peak_clocks = (uint16_t)(clocks > 0xFFFF ? 0xFFFF : clocks);
226
			f->peak_line   = i;
227
		}
228
		if (clocks > VERA_BW_LINE_CLOCKS)
229
			f->lines_over++;
230
231
		sorted[n++] = (uint16_t)(clocks > 0xFFFF ? 0xFFFF : clocks);
232
	}
233
234
	f->lines_active   = n;
235
	f->layer_bytes[0] = f->layer_fetches[0] * VERA_BW_BYTES_PER_ACCESS;
236
	f->layer_bytes[1] = f->layer_fetches[1] * VERA_BW_BYTES_PER_ACCESS;
237
238
	if (n == 0) {
239
		f->p95_clocks = f->mean_clocks = 0;
240
		return;
241
	}
242
243
	qsort(sorted, n, sizeof sorted[0], cmp_u16);
244
245
	// Integer percentile: no <math.h>, and rounding a rank does not need it.
246
	// Index (95*(n-1) + 99)/100 is the ceiling of the 95th-percentile rank.
247
	f->p95_clocks  = sorted[(95u * (n - 1u) + 99u) / 100u];
248
	f->mean_clocks = (uint16_t)(f->total_clocks / n);
249
}
250
251
void
252
vera_bandwidth_frame_end(uint32_t frame)
253
{
254
	if (!vera_bandwidth_active)
255
		return;
256
257
	cur_frame.frame = frame;
258
	summarise(&cur_frame, cur_lines);
259
260
	memcpy(&pub_frame, &cur_frame, sizeof pub_frame);
261
	memcpy(pub_lines, cur_lines, sizeof pub_lines);
262
	have_frame = true;
263
264
	memset(&cur_frame, 0, sizeof cur_frame);
265
	memset(cur_lines, 0, sizeof cur_lines);
266
}
267
268
// ─── Configuration ──────────────────────────────────────────────────────────
269
270
void
271
vera_bandwidth_set_enabled(bool enabled)
272
{
273
	if (enabled == vera_bandwidth_active)
274
		return;
275
	vera_bandwidth_active = enabled;
276
	vera_bandwidth_reset();
277
}
278
279
bool
280
vera_bandwidth_is_enabled(void)
281
{
282
	return vera_bandwidth_active;
283
}
284
285
void
286
vera_bandwidth_arm(vera_bw_owner_t owner, bool want)
287
{
288
	if ((unsigned)owner >= VERA_BW_OWNER_COUNT)
289
		return;
290
	owners[owner] = want;
291
292
	bool any = false;
293
	for (int i = 0; i < VERA_BW_OWNER_COUNT; i++)
294
		any = any || owners[i];
295
296
	vera_bandwidth_set_enabled(any);
297
}
298
299
bool
300
vera_bandwidth_owner_wants(vera_bw_owner_t owner)
301
{
302
	return (unsigned)owner < VERA_BW_OWNER_COUNT && owners[owner];
303
}
304
305
void
306
vera_bandwidth_reset(void)
307
{
308
	memset(&cur_frame, 0, sizeof cur_frame);
309
	memset(&pub_frame, 0, sizeof pub_frame);
310
	memset(cur_lines, 0, sizeof cur_lines);
311
	memset(pub_lines, 0, sizeof pub_lines);
312
	have_frame = false;
313
}
314
315
void
316
vera_bandwidth_configure(uint16_t lines)
317
{
318
	if (lines > 0)
319
		scanlines_per_frame = lines;
320
}
321
322
uint16_t
323
vera_bandwidth_scanlines_per_frame(void)
324
{
325
	return scanlines_per_frame;
326
}
327
328
// ─── Reading ────────────────────────────────────────────────────────────────
329
330
bool
331
vera_bandwidth_last_frame(vera_bw_frame_t *out)
332
{
333
	if (!have_frame || out == NULL)
334
		return false;
335
	memcpy(out, &pub_frame, sizeof *out);
336
	return true;
337
}
338
339
bool
340
vera_bandwidth_current_frame(vera_bw_frame_t *out)
341
{
342
	if (out == NULL || !vera_bandwidth_active)
343
		return false;
344
	memcpy(out, &cur_frame, sizeof *out);
345
	summarise(out, cur_lines);
346
	return true;
347
}
348
349
const vera_bw_line_t *
350
vera_bandwidth_last_lines(uint16_t *count)
351
{
352
	if (!have_frame) {
353
		if (count)
354
			*count = 0;
355
		return NULL;
356
	}
357
	if (count)
358
		*count = VERA_BW_LINES;
359
	return pub_lines;
360
}