OXYGENHUB
◆ main enhanced-x16-emulator / src / sprite_trace.h
12.7 KiB · Objective-C · last changed chore(): initial commit 25 days ago source blame history raw
9120e874 Andy McCall 1
// Commander X16 Emulator — sprite multiplexing trace
2
//
3
// VERA has 128 hardware sprite slots. A program that needs more rewrites the
4
// sprite attribute table part-way down the frame from a raster IRQ, so one slot
5
// shows a different sprite in each horizontal band of the screen. The static
6
// sprite viewer in the VERA panel reads the attribute table as it stands when
7
// you happen to look at it, which for a multiplexed frame is whatever the last
8
// band left behind -- the other 900-odd sprites the player saw are simply not
9
// in the data any more. This module records them.
10
//
11
// ─── What is recorded, and from where ──────────────────────────────────────
12
//
13
// Two streams, deliberately separate, because they answer different questions.
14
//
15
//   The renderer stream is ground truth. render_sprite_line() already walks all
16
//   128 slots for every scanline and already enforces VERA's per-line sprite
17
//   fetch budget, so it knows exactly which slot held which value when the beam
18
//   passed, which of those actually put pixels on screen, and which were cut
19
//   off when the budget ran out. Every "what did the frame contain" number here
20
//   comes from there, and none of it is reconstructed.
21
//
22
//   The mutation stream is the cause: the CPU's writes into $1FC00-$1FFFF, with
23
//   the cycle and scanline each landed on. That is what makes the CPU cost of
24
//   the multiplexing measurable, and what makes a write that missed its own
25
//   raster window detectable.
26
//
27
// Crossing the two is where the useful diagnostics live: a slot value the CPU
28
// wrote but the beam never drew is a wasted write, and a slot value written
29
// after the beam had already passed its Y position is a late write -- the
30
// classic multiplexer bug, and invisible in any view that only reads the
31
// attribute table.
32
//
33
// ─── Generations ───────────────────────────────────────────────────────────
34
//
35
// A "generation" is one value a slot held for one span of the frame. A sprite
36
// that never moves is one generation covering the whole frame; a slot recycled
37
// eight times is eight. Generations are opened lazily, when the renderer next
38
// reads a slot the CPU has dirtied, rather than on the write itself. That is
39
// what makes a partial update honest: a multiplexer storing bytes 2..5 across a
40
// scanline boundary leaves the renderer looking at a half-updated sprite for
41
// one line, and recording at read time captures the half-updated value that was
42
// really displayed instead of a coherent one that never existed.
43
//
44
// Per-generation totals are derived in sprite_trace_frame_end() by scanning the
45
// residency map, not accumulated as the frame runs. render_line() can be called
46
// more than once for the same scanline (a mid-line register split re-renders
47
// the line, and the last call wins), so running totals would count those lines
48
// twice; a single pass over the finished map cannot.
49
50
#ifndef SPRITE_TRACE_H
51
#define SPRITE_TRACE_H
52
53
#include <stdbool.h>
54
#include <stddef.h>
55
#include <stdint.h>
56
57
#ifdef __cplusplus
58
extern "C" {
59
#endif
60
61
#define SPRITE_TRACE_SLOTS 128
62
#define SPRITE_TRACE_LINES 480
63
64
// Clocks the sprite renderer gets per scanline, matching render_time_done in
65
// sprite_renderer.v:43 and SPRITE_RENDER_TIME in video.c. Kept here so the
66
// panel can draw the ceiling without reaching into video.c.
67
//
68
// It is WALL CLOCK, not bus accesses: render_time_r increments every clock
69
// (sprite_renderer.v:46-56), including the STATE_RENDER clocks when the
70
// renderer is painting the line buffer and holding no bus strobe at all. So
71
// this is the outer envelope a line's sprite work must finish inside, not a
72
// share of VRAM bandwidth -- see src/vera_bandwidth.h, which charges the same
73
// 800-clock line window that this 798 is measured against.
74
#define SPRITE_TRACE_LINE_BUDGET 798
75
76
// Marks a generation that was already in the attribute table when the frame
77
// started, rather than written during it.
78
#define SPRITE_TRACE_NO_LINE 0xFFFFu
79
80
// Per (line, slot) residency flags.
81
enum {
82
	SPRITE_TRACE_EVALUATED = 1u << 0, // the renderer reached this slot on this line
83
	SPRITE_TRACE_ONSCREEN  = 1u << 1, // enabled (z != 0) and the line fell inside its Y span
84
	SPRITE_TRACE_DREW      = 1u << 2, // put at least one non-transparent pixel on the line
85
	SPRITE_TRACE_CUT       = 1u << 3, // the line's budget ran out part-way through this sprite
86
};
87
88
// Per-generation flags.
89
enum {
90
	SPRITE_GEN_CARRIED = 1u << 0, // held over from before the frame, never written during it
91
	SPRITE_GEN_DREW    = 1u << 1, // drew on at least one line
92
	SPRITE_GEN_CUT     = 1u << 2, // was cut short by the budget on at least one line
93
	SPRITE_GEN_LATE    = 1u << 3, // written after the beam had passed its own Y span
94
};
95
96
// One value one slot held for one span of the frame.
97
typedef struct {
98
	uint8_t  attr[8];        // the attribute bytes as the renderer first read them
99
	uint8_t  slot;           // 0..127
100
	uint8_t  flags;          // SPRITE_GEN_*
101
	uint16_t index_in_slot;  // 0 for the slot's first value this frame, then 1, 2, ...
102
	uint16_t write_line;     // scanline the CPU dirtied it on, or SPRITE_TRACE_NO_LINE
103
	uint32_t write_cycle;    // clockticks6502 at that write
104
	// Attribute bytes whose value actually changed to produce this generation.
105
	// Not the number of stores the CPU made: a handler that rewrites all eight
106
	// bytes when only three differ shows 3 here and 8 in summary.mutations. The
107
	// gap between those two is redundant work the guest paid for.
108
	uint16_t writes;
109
	uint16_t first_line;     // first scanline it drew on (SPRITE_TRACE_NO_LINE if never)
110
	uint16_t last_line;      // last scanline it drew on
111
	uint16_t lines_resident; // scanlines it was the slot's value, drawn or not
112
	uint16_t lines_drawn;    // scanlines it actually put pixels on
113
	uint32_t budget_cycles;  // VERA sprite-fetch cycles it consumed over the frame
114
	// Decoded once at open, so consumers do not each re-derive it.
115
	int16_t  x, y;
116
	uint16_t width, height;
117
	uint32_t address;
118
	uint8_t  zdepth, palette_offset, color_mode, collision_mask;
119
	bool     hflip, vflip;
120
} sprite_gen_t;
121
122
// One CPU write into the sprite attribute table.
123
typedef struct {
124
	uint32_t cycle;   // clockticks6502 when it landed
125
	uint16_t line;    // display scanline the beam was on, or SPRITE_TRACE_NO_LINE off-screen
126
	uint8_t  slot;
127
	uint8_t  offset;  // 0..7, byte within the sprite's attributes
128
	uint8_t  old_val;
129
	uint8_t  new_val;
130
} sprite_mutation_t;
131
132
// What one scanline's sprite rendering cost and whether it fit.
133
typedef struct {
134
	uint16_t budget_used;   // clocks actually used, never above the ceiling
135
	uint32_t demand;        // clocks the line wanted, which may exceed it
136
	uint16_t evaluated;     // slots the renderer got to before time ran out
137
	uint16_t drawn;         // slots that put pixels on this line
138
	uint16_t dropped;       // slots on this line that time ran out before drawing
139
	uint16_t cut_slot;      // first slot time ran out in, or SPRITE_TRACE_NO_LINE
140
	uint32_t cpu_cycles;    // CPU cycles the guest spent while the beam was on this line
141
	bool     exhausted;     // the line ran out of time, so later slots were dropped
142
	bool     rendered;      // render_sprite_line() ran for this line at all
143
} sprite_line_stat_t;
144
145
// Frame-level totals, all derived in sprite_trace_frame_end().
146
typedef struct {
147
	uint32_t frame;              // frame_count this describes
148
	uint32_t cpu_cycles;         // CPU cycles spanned by the frame
149
150
	uint16_t gen_count;          // generations recorded (>= number of slots used)
151
	uint16_t effective_sprites;  // generations that actually drew: the real answer to
152
	                             // "how many sprites did the player see this frame"
153
	uint16_t phantom_writes;     // generations written during the frame that never drew
154
	uint16_t late_writes;        // generations written after their own Y span had passed
155
	uint16_t slots_used;         // slots that drew at least once
156
	uint16_t slots_multiplexed;  // slots that held more than one drawn value
157
	uint16_t max_gens_in_slot;   // deepest multiplexing on any one slot
158
159
	uint32_t mutations;          // attribute-byte writes seen
160
	uint32_t mutations_onscreen; // ...of which landed while the beam was in the display
161
	uint32_t mutation_bursts;    // runs of writes separated by <= burst_gap cycles
162
	uint32_t mutation_cycles;    // CPU cycles spanned by those runs
163
	uint32_t burst_gap;          // the gap threshold those two were computed with
164
165
	uint16_t lines_exhausted;    // scanlines that ran out of sprite render time
166
	uint32_t sprites_dropped;    // slot/line pairs that time ran out before drawing
167
	uint32_t budget_used_total;  // summed sprite render clocks over the frame
168
	uint16_t peak_line;          // scanline that wanted the most render time
169
	uint16_t peak_budget;        // that line's clocks actually used
170
	uint32_t peak_demand;        // that line's clocks wanted, ceiling or not
171
	bool     multiplexing;       // any slot held more than one drawn value
172
} sprite_frame_summary_t;
173
174
// A complete recording of one frame. Returned by sprite_trace_last_frame() and
175
// stable until the next frame is finished.
176
typedef struct {
177
	sprite_frame_summary_t summary;
178
	const sprite_gen_t    *gens;
179
	uint16_t               gen_count;
180
	const sprite_mutation_t *mutations;
181
	uint32_t                 mutation_count;
182
	uint32_t                 mutations_dropped; // lost to the ring buffer's capacity
183
	const sprite_line_stat_t *lines;            // [SPRITE_TRACE_LINES]
184
	// Residency map. line_gen[line * SPRITE_TRACE_SLOTS + slot] indexes gens[],
185
	// or is SPRITE_TRACE_NO_GEN when the renderer never reached that slot.
186
	const uint16_t *line_gen;
187
	const uint8_t  *line_flags;   // SPRITE_TRACE_* per (line, slot)
188
	const uint16_t *line_budget;  // per (line, slot) sprite-fetch cycles
189
} sprite_trace_frame_t;
190
191
#define SPRITE_TRACE_NO_GEN 0xFFFFu
192
193
// Set by sprite_trace_set_enabled(). Exposed directly so the renderer's inner
194
// loop can branch on a load instead of a call: render_sprite_line() consults it
195
// 128 times per scanline, 480 scanlines per frame.
196
extern bool sprite_trace_active;
197
198
// Turning tracing on allocates the maps (about 300 KB per buffer, two buffers);
199
// turning it off frees them. A build that never opens the panel therefore pays
200
// nothing. Returns false if the allocation failed, leaving tracing off.
201
bool sprite_trace_set_enabled(bool on);
202
void sprite_trace_reset(void);
203
204
// Bytes currently held by the trace, for the panel to report.
205
size_t sprite_trace_memory_usage(void);
206
207
// The gap, in CPU cycles, below which two consecutive attribute writes are
208
// treated as part of the same burst. Changing it re-derives mutation_bursts and
209
// mutation_cycles on the next frame.
210
void     sprite_trace_set_burst_gap(uint32_t cycles);
211
uint32_t sprite_trace_get_burst_gap(void);
212
213
// ─── Recording hooks, called from video.c ──────────────────────────────────
214
215
// Finish the frame in progress and start a new one. attrs is the live attribute
216
// table, used to seed each slot's carried-over generation.
217
void sprite_trace_frame_advance(uint32_t frame, uint32_t cpu_cycle,
218
                                const uint8_t attrs[SPRITE_TRACE_SLOTS][8]);
219
220
// A CPU write into the attribute table. Call after the byte has been stored.
221
void sprite_trace_note_write(uint8_t slot, uint8_t offset, uint8_t old_val,
222
                             uint8_t new_val, uint16_t line, uint32_t cpu_cycle);
223
224
// render_sprite_line() is starting on this line. Clears the line, so that a
225
// line rendered more than once records only its final pass -- matching the
226
// display, where the last pass is what remains on screen.
227
void sprite_trace_line_begin(uint16_t line, uint32_t cpu_cycle);
228
229
// The renderer read this slot on this line. attr is the live attribute table
230
// row, budget_used is what this sprite consumed, flags is SPRITE_TRACE_*.
231
void sprite_trace_note_slot(uint16_t line, uint8_t slot, const uint8_t attr[8],
232
                            uint16_t budget_used, uint8_t flags);
233
234
// render_sprite_line() has finished this line. `result` carries the whole line
235
// tally; the caller fills in what it measured and leaves cpu_cycles and
236
// rendered to the trace.
237
void sprite_trace_line_end(uint16_t line, const sprite_line_stat_t *result);
238
239
// ─── Reading ───────────────────────────────────────────────────────────────
240
241
// The most recently completed frame, or NULL if none has finished since tracing
242
// was enabled. Stable until the next frame boundary.
243
const sprite_trace_frame_t *sprite_trace_last_frame(void);
244
245
#ifdef __cplusplus
246
}
247
#endif
248
249
#endif // SPRITE_TRACE_H