OXYGENHUB
◆ main enhanced-x16-emulator / src / vera_bandwidth.h
17.0 KiB · Objective-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 — VERA VRAM bandwidth accounting.
2
//
3
// THE PROBLEM: src/perf_budget.h answers "how much of my frame is my CODE
4
// using". It says nothing about VERA, and VERA is the other half of the
5
// question -- a program can have cycles to spare and still tear, drop sprites
6
// or run out of time to push pixels through the data port. This module
7
// measures the VRAM bus: what the layers fetch, what the CPU pushes through
8
// $9F23/$9F24, and what VERA FX multiplies that into.
9
//
10
// ─── What the bus actually is ───────────────────────────────────────────────
11
//
12
// Quoted from the RTL, which for an FPGA is not a description of the hardware
13
// but IS the hardware. Pinned to X16Community/vera-module tag v47.0.2, the
14
// same commit tests/fetch_vera_rtl.py fetches; run it to check these.
15
//
16
//   * ONE VRAM bus, 32 bits wide, one access per clock. main_ram.v:7-10 takes
17
//     a 15-bit word address and 32 bits of data, so AN ACCESS IS FOUR BYTES.
18
//   * A scanline is 800 clocks. video_vga.v:27-31 -- 640 active + 16 front
19
//     porch + 96 sync + 48 back porch. 525 lines a frame (video_vga.v:33-37).
20
//   * FIXED PRIORITY, not round-robin: vram_if.v:142-157 is a combinatorial
21
//     if/else-if chain, if0 > if1 > if2 > if3. top.v:680-713 wires those to
22
//     the CPU-and-FX port ("Interface 0 - 8-bit (highest priority)"), layer 0,
23
//     layer 1 and the sprite renderer, in that order. SPRITES COME LAST.
24
//   * An access costs TWO clocks: the FETCH state raises the strobe and the
25
//     WAIT_FETCH state takes the ack one clock later
26
//     (layer_renderer.v:250-271).
27
//
28
// WHY THE HEADLINE IS PER-SCANLINE AND NOT "PERCENT OF PEAK": 25 MHz x 4 bytes
29
// is 100 MB/s, and a real program uses perhaps a fifth of it. Reporting that
30
// would produce a number that is always low and always useless -- the same
31
// mistake as reporting raw cycles-per-frame, which is always 100%. The 800
32
// clocks of ONE SCANLINE is the window that can genuinely run out, and at full
33
// load it exactly does:
34
//
35
//     layer 0, 8bpp bitmap 640   160 fetches x 2 = 320 clocks
36
//     layer 1, 8bpp bitmap 640   160 fetches x 2 = 320 clocks
37
//     sprites, 4bpp, full line                   = 160 clocks
38
//                                                  ----
39
//                                                   800  = H_TOTAL
40
//
41
// ─── Why this models the registers instead of counting reads ────────────────
42
//
43
// The obvious implementation -- count video_space_read() calls in the render
44
// path -- measures the wrong machine. The emulator's renderer is a PIXEL loop:
45
// it walks SCREEN_WIDTH columns and reads a byte whenever it crosses a byte
46
// boundary, so at 320-wide it reads the same byte repeatedly, and it never
47
// fetches 32 bits at once the way the hardware does. Those call counts are an
48
// artefact of the emulator's inner loop, not VERA's bus traffic.
49
//
50
// So the fetch count is derived instead: a closed form over the layer's mode,
51
// colour depth and tile geometry that reproduces the RTL's own decode tables
52
// (layer_renderer.v:42-51 pixels_per_word_minus1, :68-77
53
// words_per_line_minus1). It is both more faithful and cheaper than
54
// instrumenting every read.
55
//
56
// ─── WHERE THIS MODEL IS WRONG ──────────────────────────────────────────────
57
//
58
// Read this part before trusting a number out of it.
59
//
60
//   1. FETCHES ARE MODELLED, NOT OBSERVED. If the emulator's renderer and the
61
//      RTL disagree about what a mode fetches, this follows the RTL. That is
62
//      the intent -- the question is "what would VERA do", not "what did this
63
//      emulator's convenience code do" -- but it means a bug in the emulator's
64
//      renderer will NOT show up here.
65
//
66
//   2. CLOCKS ARE A LOWER BOUND; ACCESSES ARE EXACT. Two clocks per access is
67
//      the uncontended cost. Under real contention a lower-priority client
68
//      holds its strobe and waits, so it burns MORE clocks than this charges.
69
//      Trust the access counts; treat the clock totals as "at least this bad".
70
//
71
//   3. THE SCROLL TILE IS AN ASSUMPTION. A layer scrolled mid-tile needs one
72
//      more tile than the arithmetic gives. This charges that extra tile
73
//      whenever hscroll is not tile-aligned. The RTL's exact edge behaviour at
74
//      the last partially-visible tile was not pinned down, so the count can
75
//      be one tile high or low at the margin.
76
//
77
//   4. A LINE IS FETCHED DURING THE LINE BEFORE IT. The layer line buffer is
78
//      double-buffered (top.v active_line_buf_r, layer_line_buffer.v) and
79
//      composer.v starts the render for line N+1 as line N begins. This module
80
//      reports a line's fetches against the line they PAINT, not the line they
81
//      happen on, because that is where a developer looks for them.
82
//
83
//   5. SPRITE HEADROOM IS NOT COMPUTED HERE. This module deliberately knows
84
//      nothing about sprite_trace.c. It reports what the layers and the CPU
85
//      took, and leaves "so how much was left for sprites" to the consumer,
86
//      which is what keeps this file dependency-free. The Performance panel
87
//      crosses the two.
88
//
89
//   6. FX IS COVERED ONLY WHERE THE EMULATOR IMPLEMENTS IT. addr_data.v is
90
//      59 KB of pipeline; this charges the paths video.c actually has -- the
91
//      four-byte cache write and the affine prefetch's second fetch -- and
92
//      does not claim to be a whole FX performance model.
93
//
94
// WHY THIS FILE HAS NO EMULATOR DEPENDENCIES: it is unit-tested on its own,
95
// with no CPU, no memory, no video and no SDL -- see
96
// tests/test_vera_bandwidth.c. Everything it needs arrives as a function
97
// argument or through vera_bandwidth_configure(), exactly as perf_budget.c,
98
// io_trace.c and cpu/irq_ctx.c do, and for the same reason.
99
//
100
// AND NO <math.h>: lrintf and fabsf are intrinsics on MSVC but real libm
101
// symbols on glibc, which broke the Linux link for every target that pulls in
102
// memory.c. Everything here is integer arithmetic.
103
//
104
// COST WHEN OFF: one load and one predictable branch, from the inline guards
105
// below. The guards test the flag BEFORE evaluating anything, because C
106
// evaluates arguments before the callee can decline them.
107
//
108
// THREADING: same rule as perf_budget.h -- one thread steps the CPU, polls DAP
109
// and draws the debugger, so none of this locks. Do not call it from the SDL
110
// audio callback.
111
#ifndef _VERA_BANDWIDTH_H_
112
#define _VERA_BANDWIDTH_H_
113
114
#include <stdbool.h>
115
#include <stdint.h>
116
117
#ifdef __cplusplus
118
extern "C" {
119
#endif
120
121
// Clocks in one scanline: video_vga.v:27-31, 640 + 16 + 96 + 48. This is the
122
// window everything below is measured against.
123
#define VERA_BW_LINE_CLOCKS 800
124
125
// Bytes moved by one VRAM access. main_ram.v:7-10 is 32 bits wide, so a "fetch"
126
// is always four bytes even when the client wanted one.
127
#define VERA_BW_BYTES_PER_ACCESS 4
128
129
// Clocks one access occupies the bus for: FETCH raises the strobe, WAIT_FETCH
130
// takes the ack on the next clock (layer_renderer.v:250-271). Uncontended --
131
// see limit 2 in the header comment.
132
#define VERA_BW_ACCESS_CLOCKS 2
133
134
// Active display lines, and the ceiling on lines we keep per-frame detail for.
135
// VERA scans 525 (video_vga.v:33-37) but only 480 of them fetch.
136
#define VERA_BW_LINES 480
137
138
// Pixels the layer renderer always produces before it stops, whatever the
139
// display scale: layer_renderer.v:488, lb_wridx_r[9:7] == 3'b101 is 640.
140
#define VERA_BW_LAYER_PIXELS 640
141
142
// One layer's configuration for one scanline, in the terms the RTL decodes.
143
// Deliberately not video.c's struct video_layer_properties: this module is
144
// tested without video.c, and taking only these five fields is what makes that
145
// possible.
146
typedef struct {
147
	bool    enabled;     // layer switched on in DC_VIDEO
148
	bool    bitmap_mode;
149
	uint8_t color_depth; // 0..3, meaning 1, 2, 4 and 8 bpp
150
	uint8_t tile_width;  // 0 => 8px tiles / 320px bitmap, 1 => 16px / 640px
151
	bool    scrolled;    // hscroll is not tile-aligned, so one extra tile shows
152
} vera_bw_layer_t;
153
154
// What one scanline cost. Written per line and REPLACED, never accumulated: a
155
// mid-line register split makes video.c render the same line more than once and
156
// the last pass is what the display keeps, exactly as sprite_trace does.
157
typedef struct {
158
	uint16_t layer_fetches[2]; // 32-bit accesses, per layer
159
	uint16_t port_accesses;    // CPU data-port accesses that landed on this line
160
	bool     rendered;         // this line was drawn at all
161
} vera_bw_line_t;
162
163
// Clocks a line's traffic occupied the bus for, against VERA_BW_LINE_CLOCKS.
164
// Derived rather than stored, so it cannot drift out of step with the counts it
165
// is made of -- the same reason perf_frame_work() is derived.
166
static inline uint32_t
167
vera_bw_line_clocks(const vera_bw_line_t *l)
168
{
169
	return ((uint32_t)l->layer_fetches[0] + l->layer_fetches[1] + l->port_accesses) *
170
	       VERA_BW_ACCESS_CLOCKS;
171
}
172
173
// One completed frame.
174
typedef struct {
175
	uint32_t frame;
176
177
	// ─ Layers ─
178
	uint32_t layer_fetches[2]; // 32-bit accesses over the frame
179
	uint32_t layer_bytes[2];   // the same, times four
180
	uint16_t layer_peak_line[2];
181
	uint16_t layer_peak_fetches[2];
182
183
	// ─ The CPU's data port, which is the part a program controls ─
184
	uint32_t port_reads;   // bytes the guest read through $9F23/$9F24
185
	uint32_t port_writes;  // bytes the guest wrote
186
	uint32_t port_accesses;// VRAM accesses those cost
187
	uint32_t port_vram_bytes; // bytes that actually reached VRAM (FX multiplies)
188
189
	// ─ VERA FX ─
190
	uint32_t fx_cache_writes;   // one store that wrote four bytes
191
	uint32_t fx_affine_fetches; // the extra map read an affine read costs
192
193
	// ─ Per-scanline pressure, which is the headline ─
194
	uint16_t peak_line;    // busiest scanline
195
	uint16_t peak_clocks;  // its clocks, against VERA_BW_LINE_CLOCKS
196
	uint16_t p95_clocks;   // 95th percentile over the active lines
197
	uint16_t mean_clocks;
198
	// Lines with any bus traffic at all: rendered by a layer, touched by the
199
	// CPU port, or both. NOT just "lines a layer drew" -- a program that
200
	// switches the layers off and blasts VRAM through $9F23 is still using the
201
	// bus, and a denominator that ignored it would report that frame as idle.
202
	uint16_t lines_active;
203
	uint16_t lines_over;   // lines whose layer+CPU demand exceeded the window
204
	uint32_t total_clocks;
205
	uint32_t total_fetches;
206
} vera_bw_frame_t;
207
208
// ─── The model itself ───────────────────────────────────────────────────────
209
210
// VRAM accesses one layer performs for one scanline. Pure: no state, no
211
// globals, safe to call from anywhere, and the reason this module is testable.
212
//
213
// Tile modes fetch a map entry every two tiles, because a 32-bit word holds two
214
// 16-bit entries (layer_renderer.v:113, and the every-other-tile FETCH_MAP at
215
// :305), plus words_per_line_minus1 + 1 words of pixel data per tile
216
// (layer_renderer.v:68-77).
217
//
218
// Bitmap modes fetch no map at all, just the pixel words the width needs at
219
// pixels_per_word_minus1 + 1 pixels each (layer_renderer.v:42-51).
220
uint16_t vera_bandwidth_layer_line_fetches(const vera_bw_layer_t *layer);
221
222
// Pixels one 32-bit word holds in a given mode: layer_renderer.v:42-51.
223
// Exposed because the tests check it directly against the RTL's table.
224
uint8_t vera_bandwidth_pixels_per_word(bool bitmap_mode, uint8_t color_depth,
225
                                       uint8_t tile_width);
226
227
// Words of pixel data per tile row: layer_renderer.v:68-77.
228
uint8_t vera_bandwidth_words_per_tile_line(bool bitmap_mode, uint8_t color_depth,
229
                                           uint8_t tile_width);
230
231
// Bus clocks one sprite's pixel fetches occupy on one scanline.
232
//
233
// THE UNITS TRAP THIS EXISTS TO CLOSE. sprite_trace reports a line's sprite
234
// cost as RENDER TIME: `demand` and `budget_used` come from render_time_r
235
// (sprite_renderer.v:43-56), which ticks EVERY clock -- including the
236
// STATE_RENDER clocks when the renderer is painting the line buffer and holding
237
// no bus strobe at all. Everything this module reports is BUS OCCUPANCY. The
238
// two differ by the sprite renderer's duty cycle, roughly five to one at 4 bpp,
239
// so subtracting one from the other overstates contention badly enough to warn
240
// about lines that comfortably fit -- and a false alarm in a diagnostic is
241
// worse than no diagnostic. Convert with this before comparing.
242
//
243
// A sprite fetches one 32-bit word per 8 pixels at 4 bpp and per 4 at 8 bpp,
244
// and each fetch holds the bus VERA_BW_ACCESS_CLOCKS. `color_mode` is 0 for
245
// 4 bpp and 1 for 8 bpp, matching the sprite attribute bit.
246
//
247
// The render clocks are not lost, they are simply not this module's business:
248
// they are what the VERA panel's Multiplex -> Render time view measures, and
249
// they are what actually drops sprites.
250
uint16_t vera_bandwidth_sprite_bus_clocks(uint16_t sprite_width, uint8_t color_mode);
251
252
// ─── The hot path ───────────────────────────────────────────────────────────
253
// Read directly by the inline guards, so flipping it takes effect immediately.
254
extern bool vera_bandwidth_active;
255
256
void vera_bandwidth_line_(uint16_t line, const vera_bw_layer_t *l0,
257
                          const vera_bw_layer_t *l1);
258
void vera_bandwidth_note_port_(bool write, uint8_t vram_bytes, uint16_t line);
259
void vera_bandwidth_note_fx_cache_write_(uint16_t line);
260
void vera_bandwidth_note_fx_affine_(uint16_t line);
261
262
// A scanline was rendered with these two layer configurations. Replaces
263
// whatever the line held, so re-rendering a line after a mid-line register
264
// write records the final pass rather than the sum of the passes.
265
static inline void
266
vera_bandwidth_line(uint16_t line, const vera_bw_layer_t *l0, const vera_bw_layer_t *l1)
267
{
268
	if (vera_bandwidth_active)
269
		vera_bandwidth_line_(line, l0, l1);
270
}
271
272
// One byte moved through $9F23/$9F24. `vram_bytes` is what actually reached
273
// VRAM, which is 1 for an ordinary access and 4 for an FX cache write.
274
//
275
// Do NOT call this for the debugger's own reads. video.c short-circuits those
276
// on debugOn, and charging them would mean opening a memory view changed the
277
// number the developer is trying to read.
278
static inline void
279
vera_bandwidth_note_port(bool write, uint8_t vram_bytes, uint16_t line)
280
{
281
	if (vera_bandwidth_active)
282
		vera_bandwidth_note_port_(write, vram_bytes, line);
283
}
284
285
// An FX cache write: one CPU store that put four bytes into VRAM.
286
static inline void
287
vera_bandwidth_note_fx_cache_write(uint16_t line)
288
{
289
	if (vera_bandwidth_active)
290
		vera_bandwidth_note_fx_cache_write_(line);
291
}
292
293
// An affine-mode read, which costs a second fetch to translate the tile map.
294
static inline void
295
vera_bandwidth_note_fx_affine(uint16_t line)
296
{
297
	if (vera_bandwidth_active)
298
		vera_bandwidth_note_fx_affine_(line);
299
}
300
301
// Close the frame in progress and publish it. Cheap enough to call
302
// unconditionally; it returns immediately when accounting is off.
303
void vera_bandwidth_frame_end(uint32_t frame);
304
305
// ─── Configuration ──────────────────────────────────────────────────────────
306
// Ownership mirrors perf_budget: accounting runs while ANY owner wants it, so
307
// closing the panel does not strand a DAP client and a client disconnecting
308
// does not switch off a panel somebody is reading.
309
typedef enum {
310
	VERA_BW_OWNER_UI = 0,
311
	VERA_BW_OWNER_DAP,
312
	VERA_BW_OWNER_REMOTE,  // an RRDC control-port client (GET/POST /perf)
313
	VERA_BW_OWNER_COUNT
314
} vera_bw_owner_t;
315
316
void vera_bandwidth_arm(vera_bw_owner_t owner, bool want);
317
bool vera_bandwidth_owner_wants(vera_bw_owner_t owner);
318
319
void vera_bandwidth_set_enabled(bool enabled);
320
bool vera_bandwidth_is_enabled(void);
321
void vera_bandwidth_reset(void);
322
323
// Lines the machine actually scans, so a consumer can report the right
324
// denominator without reaching into video.c.
325
void     vera_bandwidth_configure(uint16_t scanlines_per_frame);
326
uint16_t vera_bandwidth_scanlines_per_frame(void);
327
328
// ─── Reading ────────────────────────────────────────────────────────────────
329
// The most recently completed frame. False when none has finished since
330
// accounting was switched on.
331
bool vera_bandwidth_last_frame(vera_bw_frame_t *out);
332
333
// The frame still being drawn, for a live readout that does not wait for the
334
// boundary. Percentiles over a partial frame cover the lines drawn so far.
335
bool vera_bandwidth_current_frame(vera_bw_frame_t *out);
336
337
// Per-line detail for the most recently completed frame. Stable until the next
338
// frame boundary. Returns NULL when there is no completed frame.
339
//
340
// This covers the ACTIVE lines only. Vertical blanking is real bus time and the
341
// CPU can use it -- that traffic is in the frame totals, but it has no line to
342
// be drawn against, so it is not in here.
343
const vera_bw_line_t *vera_bandwidth_last_lines(uint16_t *count);
344
345
#ifdef __cplusplus
346
}
347
#endif
348
349
#endif // _VERA_BANDWIDTH_H_