OXYGENHUB
◆ main openmsx / doc / internal / vdp-vram-timing / 2026-measurement-analysis.md
55.8 KiB · Markdown · last changed chore(): initial commit 1 month ago renderedsource blame history raw
e8555c89 Andy McCall 1
# V9938 command timing against the 2026 logic-analyzer measurements
2
3
In 2026 a second set of logic-analyzer captures of the V9938-VRAM bus was made, on a
4
Philips NMS 8280, and published in the `part2` subdirectory of
5
<https://github.com/m9710797/vdp-timing-measurements>. Unlike the 2013 set, it covers
6
all three access-slot patterns (display off, display on with sprites off, display on
7
with sprites on) with roughly equal weight, and it includes captures with the CPU
8
accessing VRAM at the same time.
9
10
This document fits openMSX's command-engine timing model to that data. It replaces the
11
earlier reasoning from the 2013 set plus aggregate `vdpcmdx` frame counts, discussed in
12
<https://github.com/openMSX/openMSX/issues/2057> and
13
<https://github.com/sndpl/openMSX/pull/5>; the fixes that came out of that discussion
14
are commits 592ce968c and 7dde62b3f. Everything section 4 and section 6 describe is
15
implemented, in commit 5b0327eaf; the "openMSX today" rows below are measured against
16
the state before it.
17
18
Two scripts in this directory produce every number here:
19
20
```
21
vcd-slot-grid.py <measurement-repo>/part2                  # where the slots are
22
2026-measurement-check.py <repo>/part2/5.slots             # command timing
23
2026-measurement-check.py <repo>/part2/5.slots --cpu       # under CPU contention
24
2026-measurement-check.py <repo>/part2/5.slots --plain     # without section 4's rules
25
cpu-arbitration-model.py <measurement-repo>                # the CPU's own slots
26
```
27
28
Cycle numbers throughout are openMSX's, which are based on /RAS. The `.txt` analyses in
29
the measurement repository are based on /CAS, which normally follows /RAS by one cycle —
30
but not for two slots per table, see section 2.
31
32
## 1. The model under test
33
34
openMSX implements exactly the model Wouter derived in 2013 (`slots3.txt`):
35
36
* VRAM access slots are at fixed positions in a display line; which positions depends
37
  on the screen mode and on whether display and sprites are enabled
38
  (`VDPAccessSlots.cc`).
39
* A slot is allocated ~16 cycles in advance. If a CPU request is pending at that
40
  moment the CPU gets the slot; otherwise the command engine gets it if it has a
41
  request pending; otherwise the slot goes unused.
42
* The engine posts its request for access *k+1* a fixed number of cycles after access
43
  *k* completed. That number depends on the command and on which step of the command
44
  it is.
45
46
openMSX folds the arbitration latency into the per-step number, so one `Delta` value
47
per command step covers both:
48
49
> next access = the first access slot at or after (previous access + delta)
50
51
For a single observed pair of consecutive engine accesses (at *p* and *n*), that model
52
holds for every delta in `[prevSlot(n) - p + 1, n - p]`. Intersecting those intervals
53
over every observation of one step gives the step's admissible band. An empty band
54
means no single value can reproduce the data — the interesting case.
55
56
## 2. The access slot tables are confirmed, measured from /RAS
57
58
`part2/README.md` proposes moving two slots per table one cycle later. openMSX's tables
59
need no change, and the apparent disagreement is a difference of convention, not an
60
error on either side — see the end of this section.
61
62
The published `.txt` analyses derive access times by interpolating between the eight
63
DRAM refresh accesses of a line, which is only eight anchors per line. `/RAS` is a much
64
better time base: it pulses once per potential access slot whether or not the VDP uses
65
the slot, and its pattern repeats every display line. `vcd-slot-grid.py` in this
66
directory measures the grid directly from the raw `part2/1.vcd` captures:
67
68
* **Line period.** Exactly one gap per line is larger than all the others, so the sample
69
  distance between successive occurrences of it is the line period. Fitted per capture
70
  this gives 5131.13 to 5131.18 samples, very stable. (Taken literally that makes a VDP
71
  cycle 46.89 ns where 21.477 MHz is 46.57 ns, so the analyser runs about 0.7 % above
72
  its nominal 80 MHz, or the machine's crystal is off by that much. It cancels, because
73
  everything is normalised by the measured period.)
74
* **Numbering.** The refresh accesses are 128 cycles apart with the row address
75
  incrementing by one and the bank alternating, and between the last of one line and the
76
  first of the next there are 472 cycles, so the chain has a unique start. That one is
77
  at cycle 284 — the slot openMSX omits between 276 and 292.
78
* **Precision.** Over 40 captures each slot is averaged over at least 249 RAS edges with
79
  a standard deviation of at most 0.31 cycles, so each mean is good to ±0.02.
80
81
Result:
82
83
| mode | slots per line | gap structure | worst deviation of an openMSX slot |
84
|---|---|---|---|
85
| display off | 166 | 163 × 8 + 2 × 10 + 1 × 44 = 1368 | 0.013 cycles |
86
| sprites off | 132 | 6 × 66 + 8 × 29 + 10 × 2 + 12 + 20 × 32 + 24 + 44 = 1368 | 0.025 cycles |
87
| sprites on | 126 | irregular (sprite fetches) | 0.055 cycles |
88
89
All 154 / 88 / 31 openMSX slots are confirmed. Two further checks that the numbering is
90
not off by one: openMSX's display-off table is *exactly* the measured 166-slot grid minus
91
the eight refresh slots (284 + 128k) and the four dummy-read slots (1236, 1244, 1252,
92
1260), and shifting the anchor by one cycle makes both of those holes stop lining up.
93
94
Of the 104 211 accesses in the published `5.slots` files, only 1 108 (1.06 %) sit at a
95
cycle that is not a /RAS slot, and they are exactly the two slots of the proposed
96
correction (display off CAS 1326 and 1336, sprites off CAS 1324 and 1334; sprites on has
97
none). So the retiming is otherwise correct to the cycle, including across the
98
1181 → 285 stretch where there are no refresh accesses to interpolate between.
99
100
**Why the two conventions disagree there.** /CAS normally follows /RAS by one cycle. For
101
exactly those slots it follows by two:
102
103
| mode | single-/CAS accesses | with a 1-cycle /RAS → /CAS delay | with 2 cycles |
104
|---|---|---|---|
105
| display off | 4 862 | 4 839 | 23, all at RAS 1324 and 1334 |
106
| sprites off | 4 382 | 4 336 | 46, all at RAS 1322 and 1332 |
107
| sprites on | 8 036 | 8 036 | none |
108
109
(Burst accesses — one /RAS with several /CAS — are excluded; those are the display
110
fetches.) So openMSX's /RAS-based 1324 / 1334 and the measurement repository's /CAS-based
111
1326 / 1336 describe the same two slots, and the only wrong assumption was that the
112
conversion between them is a uniform one cycle. `2026-measurement-check.py` subtracts two
113
rather than one for those slots.
114
115
That the extra /CAS delay lands on exactly the two slots that start the 10-cycle gaps is
116
not a coincidence: they are the stretched memory cycles of section 4.1.
117
118
## 3. Results
119
120
508 traces without CPU activity yield 76 766 engine-to-engine transitions. Under the
121
plain model — the delay counted in VDP cycles — six steps have no value that fits, and
122
the three sprites-on steps in bold are disjoint from the other two modes:
123
124
| command | step | display off | sprites off | sprites on |
125
|---|---|---|---|---|
126
| HMMV | W → W        | [45,48]  | [45,48]  | [33,52]  |
127
| HMMV | newline      | [101,104]| [103,104]| [97,116] |
128
| LMMV | R → W        | [20,24]  | [21,24]  | [19,26]  |
129
| LMMV | W → R        | [69,72]  | [71,72]  | [71,78]  |
130
| LMMV | newline      | [133,132]! | [133,132]! | [133,148] |
131
| YMMM | R → W        | [20,24]  | [21,24]  | [9,26]   |
132
| YMMM | W → R        | [37,40]  | [34,38]  | [33,52]  |
133
| YMMM | newline      | [101,104]| [103,104]| [97,118] |
134
| HMMM | R → W        | [20,24]  | [21,24]  | [9,26]   |
135
| HMMM | W → R        | [61,60]! | [61,60]! | [59,64]  |
136
| HMMM | newline      | [125,128]| [129,128]! | **[133,134]** |
137
| LMMM | Rsrc → Rdst  | [29,32]  | [28,32]  | **[33,52]** |
138
| LMMM | Rdst → W     | [20,24]  | [21,24]  | [9,26]   |
139
| LMMM | W → Rsrc     | [61,60]! | [61,60]! | [59,64]  |
140
| LMMM | newline      | [125,128]| [129,128]! | **[131,148]** |
141
| LINE | R → W        | [18,24]  | [21,24]  | [9,26]   |
142
| LINE | W → R        | [85,84]! | [85,84]! | [79,90]  |
143
| LINE | newline      | [117,120]| [119,122]| [117,128]|
144
145
`!` marks a band that is empty by one cycle. `newline` is the step from the last write
146
of one line of the rectangle to the first read of the next. Section 5 shows that all of
147
the `!` cases and all three of the bold ones come from two mechanisms, not from noise,
148
and that fixing the first collapses the second.
149
150
## 4. What the model needs
151
152
Two refinements to the plain model account for everything but 32 of the 76 766
153
transitions; the third, in section 6, accounts for those.
154
155
### 4.1 The delay is counted in memory cycles, not VDP cycles
156
157
The measured display-off grid is 8-cycle regular except for two gaps of 10 in the
158
blanking region — `... 1316 1324 1334 1344 ...` — i.e. **two memory cycles per line are
159
stretched by two cycles each**. Every one of the six `!` bands in section 3 is a pair of
160
observations that straddle that stretch differently. The clearest case, HMMM's write →
161
read, decoded straight from `scr5-dispOff-hmmm-noCpu-1.vcd` with no interpolation:
162
163
```
164
  80 R 1bd55    104 W 1fd55    164 R 1bd56      W -> R = +60
165
1268 R 1bd62   1292 W 1fd62   1360 R 1bd63      W -> R = +68
166
```
167
168
From 1292 there *is* a slot at +60 (1352) and the engine skipped it; from 104 the slot at
169
+60 (164) was taken. The difference is that 1292 → 1352 crosses both stretched memory
170
cycles and 104 → 164 crosses neither.
171
172
> If the engine's delay counter does not see the stretch — i.e. it counts memory cycles
173
> rather than VDP cycles — the conflict disappears. 1292 → 1352 is then 56 engine cycles,
174
> so a minimum of 57..60 skips it and takes 1360 (64), while 104 → 164 is 60 either way.
175
176
The 44-cycle gap is *not* a stretch: it is five memory cycles that offer no slot, and it
177
counts in full, which is what HMMV needs (from 120 a 45..48 minimum skips 164 at +44 and
178
takes 172 at +52).
179
180
Note the counter has to be read as restarted at every access: the correction loses four
181
cycles per line, so it is well defined for an interval but not as a periodic remapping of
182
the line. That is exactly how the engine behaves — it is a delay from the previous
183
access, not a position in the line.
184
185
### 4.2 With sprites on, every delay is a cycle or two longer
186
187
With 4.1 in place, every step becomes internally consistent in every mode, and the steps
188
that still need a sprites-on distinction all need the *same* extra amount. So one rule
189
replaces the three per-command special cases: **with sprite rendering active every
190
command-engine delay is one or two cycles longer.** It reads like the arbitration decision
191
for a slot being taken earlier while the sprite fetch logic is active — the CPU
192
arbitration wants something similar (section 8.2). It cannot be a slot-position effect:
193
shifting the sprites-on slots would leave intervals within that mode unchanged.
194
195
The measurements admit 1 and 2 and cannot separate them, because the sprites-on bands are
196
the widest of the three modes. openMSX uses 1. Either value forces LMMM's
197
source→destination read and the line transition to the round 32 and 128 that the other
198
two modes already want.
199
200
### 4.3 The resulting values
201
202
| command | step | admissible | chosen | openMSX before |
203
|---|---|---|---|---|
204
| HMMM, LMMM (dst), LMMV, YMMM, LINE | R → W | 21..24 | 24 | 24 |
205
| HMMM, LMMM | W → R(src) | 59..60 | **60** | 64 |
206
| LMMM | R(src) → R(dst) | 27..32 | **32** | 32, and 48 with sprites on |
207
| HMMV | W → W | 45..48 | **46** | 48 |
208
| LMMV | W → R | 71..72 | **72** | 72 |
209
| YMMM | W → R | 33..38 | **36** | 38 |
210
| LINE | W → R | 81..84 | **84** | 88 |
211
| HMMV | newline | 103..104 | **104** | 104 |
212
| YMMM | newline | 103..104 | **104** | 104 |
213
| LINE | newline | 119..120 | **120** | 120 |
214
| HMMM, LMMM | newline | 125..128 | **128** | 128, and 134 with sprites on |
215
| LMMV | newline | 129..132 | **130** | 134 |
216
217
The values in the `chosen` column are Wouter's, picked so that each command has one
218
per-line overhead that all of its steps share: HMMV 46 + 58 and 104 = 46 + 58, LMMV
219
24 + 72 + 58, YMMM 24 + 36 + 68, HMMM 24 + 60 + 68, LMMM 32 + 24 + 60 + 68, LINE
220
24 + 84 + 36. Those constraints have almost no freedom left in them: a shared *even*
221
overhead for the two fills forces 58 and with it HMMV 46 and LMMV 72, a shared even
222
overhead for the three copies forces 68 and with it YMMM 36 and HMMM/LMMM 60. Only
223
R → W ∈ {22, 24} and LINE ∈ {84 + 36, 82 + 38} are left, and both are settled by
224
preferring the multiple of 4. The fills and the copies cannot share one overhead --
225
[57,59] and [68,69] are disjoint -- and no assignment makes every value a multiple of 8.
226
227
| model | mispredicted of 76 766 |
228
|---|---|
229
| openMSX today | 456 (0.594 %) |
230
| 4.1 + 4.2, with the deltas above | 32 (0.042 %) |
231
| and section 6 | **0** |
232
233
The rules go together: the deltas above *without* 4.1 give 1970 (2.6 %). The same model
234
gets the 2013 set right as well -- 3965 transitions, 0 wrong -- once the truncation of
235
`screen5screenoffHMMVnocpu.txt` is taken into account.
236
237
Both rules are free at run time: they only change how `VDPAccessSlots.cc` builds its
238
per-cycle lookup tables — the stretch as a correction inside the table generator, the
239
sprites-on cycle as a per-table offset — and they remove the mode conditionals from the
240
command implementations entirely. See the `Timing` struct in `VDPAccessSlots.cc`. The character, text and MSX1 tables are left alone, having never
241
been measured this way.
242
243
`vdpcmdx` improves as well, which it did not have to, since none of this was fitted to
244
it. Active area, non-CPU columns, before → after (real hardware in brackets):
245
246
| cell | before | after | real |
247
|---|---|---|---|
248
| HMMM portrait `NO SPR`  | 2636 | **2658** | 2659 |
249
| HMMM landscape `NO SPR` | 2668 | **2672** | 2673 |
250
| LMMM landscape `NO SPR` | 1970 | **1971** | 1971 |
251
| LMMM portrait `NO SPR`  | 1954 | **1955** | 1955 |
252
| YMMM landscape `NO SPR` | 3796 | **3797** | 3797 |
253
| YMMM landscape `NO SCR` | 3994 | 3995 | 3996 |
254
255
Every non-CPU cell is now within 3 pixels of the reference. Nothing else in the non-CPU
256
columns changed.
257
258
### 4.4 Where the padding is, per mode
259
260
The stretch of 4.1 is the VDP padding its line out to 1368 cycles. Measured from /RAS over
261
the whole blanking region, not just the slots the command engine can use, the total is 4
262
cycles in every bitmap mode, but it is not distributed the same way:
263
264
| mode | ordinary spacing there | padded memory cycles | total |
265
|---|---|---|---|
266
| display off | 8 | two of 10, completing at 1334 and 1344 | +2 +2 |
267
| sprites off | 8 | two of 10, completing at 1332 and 1342 | +2 +2 |
268
| sprites on | 13, 6, 10 | 15, 7, 11, completing at 1330, 1337, 1348 | +2 +1 +1 |
269
270
The sprites-on row was guesswork until the R#9 S1/S0 captures arrived, because in that
271
mode the padded cycles are sprite fetches rather than command slots. Section 10 shows how
272
those captures settle both the positions and the total.
273
274
Getting it right matters more for the positions than for the total: with the sprites-on
275
padding assumed to sit at 1332 and 1342 like the other two modes, 25 of the 16416 entries
276
of the sprites-on lookup table come out differently. The command traces cannot separate
277
the two — both reproduce every transition — but the /RAS comb can.
278
279
## 5. History: the parameter-only fix
280
281
Superseded by section 4, kept because the `vdpcmdx` comparison below is the only
282
end-to-end check of the whole chain. Two of openMSX's values were outside the admissible
283
band of the plain model and were corrected first:
284
285
| step | was | now | plain-model band |
286
|---|---|---|---|
287
| YMMM `W → R` | 36 | 38 | [37,38] |
288
| LMMV newline | 136 (= 72 + 64) | 134 (= 72 + 62) | [133,134] |
289
290
and the sprites-on deviation known from LMMM's destination read applies to the
291
line-transition step of both HMMM and LMMM as well, so that step used 134 instead of 128
292
when sprite rendering was active. Under section 4 all three become consequences of the
293
two table properties rather than separate facts.
294
295
`vdpcmdx` barely notices, as expected. Measured on a Philips NMS 8255, the ACTIVE block,
296
non-CPU columns, before → after:
297
298
| cell | master | patched | real |
299
|---|---|---|---|
300
| LMMM landscape `NORMAL` | 1340 | **1339** | 1339 |
301
| LMMM portrait `NORMAL`  | 1333 | 1331 | 1332 |
302
| HMMV landscape `NORMAL` | 4003 | 4002 | 4005 |
303
| YMMM portrait `NORMAL`  | 2093 | **2094** | 2095 |
304
305
and every other non-CPU cell is unchanged. The line-transition step happens once per
306
rectangle line, so it is worth well under a pixel per frame; and shifting the engine by
307
one 32-cycle slot at a line transition mostly just moves it to another phase of the same
308
128-cycle pattern, at the same throughput. An offline simulation of openMSX's own model
309
over the 192-line active window gives the same answer: HMMM ±0, LMMM landscape −1. The
310
HMMV and YMMM cells above cannot be affected by any of the three changes, so those two
311
±1 differences are cross-talk: `vdpcmdx` runs its tests back to back, so a change in one
312
test shifts the phase at which the next one starts. Two runs of the same binary are
313
bit-identical, so this is not emulator non-determinism.
314
315
The `NORMAL+CPU` column moves by up to 190 pixels in one cell for the same reason,
316
amplified: with the CPU taking a slot every 72 cycles the engine's slot selection is
317
chaotically sensitive to its starting phase. That column cannot judge this change (nor,
318
as Wouter has pointed out, can its hardcoded `REAL` values judge anything at the
319
few-percent level).
320
321
So the justification for these changes is the bus captures, not the aggregate frame
322
counts.
323
324
## 6. Slots that follow another slot after only 6 cycles
325
326
The sprites-off display area has its slots in pairs 6 cycles apart, at offsets
327
{0, 6, 32, 38, 64, 70, 96} of every 128 cycles. Reading the raw captures, the pair is
328
the *freed sprite fetch* and the memory cycle that is spare in both modes: with sprites
329
on, the cycle at 182 + 32k fetches sprite data and the one at 188 + 32k is free; with
330
sprites off, both are free. 25 of the 88 sprites-off slots are the second of such a
331
pair. Neither the display-off nor the sprites-on table has any.
332
333
Two independent effects follow from that, and both are needed:
334
335
**The command engine pays one cycle.** A step whose *previous* access was in one of
336
those 25 slots takes one cycle longer, as if that memory cycle -- squeezed against its
337
predecessor -- had finished one cycle late. 1778 transitions in the 2026 set start from
338
such a slot: 32 of them (HMMM's and LMMM's line transition from cycle 188) need the extra
339
cycle, and 1281 of them rule out a third cycle. So the band is exactly [1, 2]; openMSX
340
adds 1. Without it those 32 are the only mispredictions left in the whole set.
341
342
**The CPU never gets one.** Of 4550 CPU accesses in the sprites-off captures of both
343
measurement sets, **zero** are in one of those 25 slots, while all 63 other slots are
344
used. That is not a coincidence of the request phase: with the requests arriving 71.5
345
cycles apart against a 32-cycle slot pattern, an unconstrained model puts 11 % of them
346
there.
347
348
What happens instead is visible in the traces. Every unclassified read of `0x1ffff`
349
outside the four-slot dummy-read block of a normal line -- 335 of them, in the
350
sprites-off CPU captures and nowhere else -- sits in one of those 25 slots and is
351
followed by a CPU access in the next slot (322 at +26, 10 at +54). So the VDP *does* give
352
the slot to the CPU; it just cannot carry a CPU access, spends the memory cycle on a
353
dummy read, and does the access one slot later.
354
355
That also explains the last of the engine mispredictions in the CPU captures: the engine
356
was not skipping a free slot, the slot was occupied by that dummy read.
357
358
## 7. Traces that were mislabelled
359
360
`scr5-dispOff-hmmv-noCpu-nx2-6d`, `scr5-dispOff-lmmv-noCpu-nx2-3d`,
361
`scr5-dispOff-ymmm-noCpu-nx12-1d`, `scr5-sprOff-lmmm-noCpu-nx2-3d` and
362
`scr5-sprOff-lmmv-noCpu-nx8-5d` were named `noCpu` but contained CPU accesses. Before
363
excluding them they were the *only* source of inconsistency in the middle of a display
364
line -- every other anomaly is at the blanking boundary or is the sprites-on effect of
365
section 4.2. They have since been fixed or renamed to `scr5-wrong-*` in the measurement
366
repository, and the `d` sets have been relabelled.
367
368
Two accesses in the CPU captures still carry the wrong code, and they are the only
369
remaining mispredictions of the engine model there:
370
371
* `scr5-sprOn-hmmm-rdCpu-1.txt` has no `R.r` at all: the CPU's reads (0x1849a upwards)
372
  are coded `R.s`, together with the command's own source reads (0x1a1c2 upwards).
373
* `scr5-sprOn-hmmv-wrCpu-3.txt` has one `R.s 19e91` at cycle 4452 which belongs to the
374
  CPU's write sequence (…19e90, 19e91, 19e92…) -- HMMV has no source read.
375
376
One labelling subtlety is not a mistake: the unclassified read of `0x1ffff` is the VDP's
377
dummy read in most traces, but in a few YMMM captures the command's own source pointer
378
walks through the top of VRAM and produces genuine command accesses at that address. The
379
check script distinguishes the two by looking at the rest of the trace.
380
381
## 8. The CPU side
382
383
### 8.1 The engine model under contention
384
385
The `rdCpu` / `wrCpu` traces give 12 302 engine transitions with CPU accesses about 71.5
386
cycles apart, and the CPU takes the slot the engine wanted in **30 %** of them. Applying
387
openMSX's rule
388
389
> the engine takes the first slot at or after its minimum that the CPU did not take
390
> (`VDPCmdEngine::stealAccessSlot`)
391
392
to the observed CPU access times:
393
394
| | wrong |
395
|---|---|
396
| openMSX today | 90 (0.732 %) |
397
| the model of sections 4 and 6 | **3 (0.024 %)** |
398
| the same, but the loser re-arms its full delay instead of taking the next slot | 3086 (25.1 %) |
399
400
and the remaining 3 are the two mislabelled accesses of section 7. So `stealAccessSlot`
401
is right, including the detail that a command engine which loses a slot takes the *next*
402
slot rather than re-arming its full delay. The hypothesis floated earlier in
403
[sndpl/openMSX#5]https://github.com/sndpl/openMSX/pull/5 -- that under contention real
404
hardware uses slots closer together than the command's own minimum spacing -- is
405
refuted.
406
407
### 8.2 The CPU's own model
408
409
`cpu-arbitration-model.py` fits the other half: which slot a CPU port access ends up in.
410
The two measurement sets are complementary. The 2013 machine (NMS 8250) has a single
411
clock, so the port accesses sit on an exact grid with one unknown phase per capture. The
412
2026 machine (NMS 8280) has separate CPU and VDP clocks, but /CSR and /CSW were recorded,
413
so the pace of the port accesses is measurable.
414
415
The model:
416
417
1. **A constant delay** between the port access and the moment the arbiter sees the
418
   request. This is wall-clock time -- it is a pin-to-register delay, not something the
419
   memory sequencer counts -- and it is not separable from the fitted phase, so it is
420
   simply absorbed into it.
421
2. **A lookahead of 16 memory cycles at the arbiter.** The CPU gets the first eligible
422
   slot at least that far away, counted in the VDP's memory cycles with exactly the same
423
   padding subtraction as the command engine's delays.
424
3. **One request register.** A port access arriving while it is occupied is lost -- no
425
   VRAM access happens for it, and the VDP's VRAM pointer does not advance, so the CPU
426
   sees the previous byte again. It stays occupied until 2 cycles after the access.
427
   openMSX models this (`VDP::scheduleCpuVramAccess`, `tooFastCallback`); what the
428
   measurements add is that the *old* request wins, which is also what openMSX does.
429
4. A slot that follows another slot after 6 cycles is decided 2 cycles earlier than the
430
   rest, and carries the dummy read of section 6 instead of the CPU's access.
431
432
The 2013 set constrains the lookahead and the dead window only through their sum: any
433
(lookahead, dead window) with lookahead + dead = 18 and lookahead ≤ 16 fits it exactly.
434
16 + 2 is the top of that band and matches the 16 cycles openMSX already uses
435
(`Delta::CPU_16`).
436
437
Fit:
438
439
| set | CPU accesses | not predicted | predicted but absent |
440
|---|---|---|---|
441
| 2013, exact grid, one phase per capture | 1141 | **0** | **0** |
442
| 2026, fitted pace, one offset per capture | 19733 | 158 (0.80 %) | 126 |
443
444
For the 2026 set the requests are not taken from the individual /CSx edges but from a
445
straight line fitted through the whole capture: the Z80's own crystal makes the port
446
accesses a perfectly regular sequence in Z80 time, and one line through ~115 of them has
447
a residual of 0.15 cycles, well under the analyzer's 0.27-cycle sampling step. The fitted
448
rate is **5.96113 ± 0.00004 VDP cycles per Z80 T-state**, where a single-clock machine
449
would give exactly 6 -- the Z80 of that NMS 8280 runs 0.65 % fast relative to its VDP.
450
Two different test programs (40 port accesses 12 T-states apart, and 20 of them 37
451
T-states apart) give the same rate to 1 part in 10^5.
452
453
The VDP can only act on an asynchronous /CSx at one of its own clock edges, so the request
454
times are rounded up to whole VDP cycles. That is worth something: rounding to 1 cycle
455
beats no rounding, rounding to 2, and rounding to 4, in that order. So the VDP samples
456
/CSx on a 1-cycle grid.
457
458
Per pace and mode, with the constant of point 1 fitted per capture. The four loops are
459
`IN`/`OUT` x40 (72 cycles apart), `IN ; NOP ; NOP` (132), `IN` x20 slow (222), and
460
`IN ; OUT` alternating (r/w):
461
462
| pace | mode | accesses | not predicted | lost requests | constant |
463
|---|---|---|---|---|---|
464
| 72 | display off | 7729 | 74 (0.96 %) | 0 of 7833 | 9.86 ± 0.12 |
465
| 72 | sprites off | 6378 | 24 (0.38 %) | 22 of 6443 | 9.86 ± 0.13 |
466
| 72 | sprites on | 3703 | 57 (1.54 %) | 305 of 4022 | 12.05 ± 2.01 |
467
| 132 | display off | 368 | 1 (0.27 %) | 0 of 374 | 9.97 ± 0.08 |
468
| 132 | sprites off | 374 | 1 (0.27 %) | 0 of 376 | 9.62 ± 0.35 |
469
| 132 | sprites on | 991 | 2 (0.20 %) | 0 of 1000 | 11.40 ± 0.55 |
470
| 222 | display off | 675 | 1 (0.15 %) | 0 of 685 | 9.75 ± 0.22 |
471
| 222 | sprites off | 608 | **0** | 0 of 610 | 9.72 ± 0.30 |
472
| 222 | sprites on | 642 | 2 (0.31 %) | 0 of 646 | 10.55 ± 1.12 |
473
| r/w | display off | 681 | 10 (1.47 %) | 0 of 691 | 9.83 ± 0.13 |
474
| r/w | sprites off | 568 | 1 (0.18 %) | 1 of 577 | 9.92 ± 0.20 |
475
| r/w | sprites on | 636 | 2 (0.31 %) | 50 of 690 | 11.86 ± 0.26 |
476
477
**The constant is a per-mode number**: 9.8 ± 0.1 with the display off and with sprites
478
off, across four loops and three CPU paces, and about 1.9 cycles more with sprites on.
479
That is the same direction and about the same size as the command engine's extra cycle
480
in that mode.
481
482
**Reads and writes are the same.** The `r/w` captures alternate `IN A,(#98)` and
483
`OUT (#98),A`, so a difference between the two would show up as every other request being
484
misplaced. Fitting a separate constant for writes puts the difference at **0.0 cycles**,
485
with 13 mispredictions of 1885 at 0.0 against 32 at −1 and 52 at +1: the same delay to
486
within about a third of a cycle.
487
488
The 222-cycle captures are the ones with a slow CPU loop, made specifically to remove the
489
lost requests, and they do: 3 of 1924 accesses mispredicted, and one whole mode exact.
490
The constant repeats to ±0.01 cycles between display-off and sprites-off and to ±0.15
491
between the two paces, which is a good check on the whole chain -- the analyzer time base,
492
the slot grid, the pace fit and the model. Sprites-on wants 1 to 2 cycles more, in the
493
same direction as the command engine's extra cycle in that mode, but with a much larger
494
scatter because that mode has the fewest slots per line.
495
496
Of what is left, about a sixth are the *first* access of their capture, some fifteen times
497
the rate the remaining positions show. That is the one thing the model cannot know: a
498
capture starts at an arbitrary point in the test program, so there may already be a
499
request in flight. Almost all the others are isolated single slots.
500
501
## 9. What would help next
502
503
* **Command startup cost.** Still never measured. It is invisible to `vdpcmdx` (one
504
  command per frame of 10240 pixels). `part2` contains three `*-start-*` captures that
505
  reach step 4 but not `5.slots`; without knowing what the capture was triggered on they
506
  cannot be turned into a number, but if that is recoverable they would be the first
507
  direct measurement of it. Otherwise: sync to the line interrupt, burn a programmable
508
  number of cycles, start a one- or two-pixel command, poll S#2 once. The same rig would
509
  test section 4.1 directly, since it predicts that a command whose next access lands
510
  just after the padding behaves differently from one that does not cross it.
511
* **Alternating read and write.** Nothing in either set can tell whether a read and a
512
  write reach the arbiter with the same delay: each capture is all reads or all writes,
513
  so any difference disappears into the fitted constant. A loop that alternates them
514
  would separate the two.
515
* **Sprites-on with a slow loop and more slots.** The sprites-on constant of section 8.2
516
  is still the loosest number in the model (±1.2 cycles even at the slow pace), simply
517
  because that mode has 31 slots per line. Splitting the lookahead from the constant
518
  delay -- the two are only separable where the padding falls inside the lookahead window
519
  -- would need captures with requests deliberately placed near cycle 1330.
520
* **The 1365-cycle modes.** Section 10 measures the line length and where the padding
521
  goes, but openMSX does not implement those modes at all. If it ever does, the
522
  prediction is that the command engine needs no padding correction there.
523
524
## 10. Where the padding comes from: R#9 S1/S0 and R#18, measured
525
526
The V9938 data book documents bits S1 and S0 of R#9 as selecting one of three "cycle
527
modes", and gives 1368 cycles per horizontal line for S1,S0 = 0,0 but 1365 for the other
528
two settings (section 5 "Cycle mode" and section 7-1 "Horizontal display parameters").
529
The `scr5-spr{On,Off}-hmmv-noCpu-s{0,16,32,48}-*e.vcd` captures test that directly. Taking
530
the line period and the refresh interval both in analyzer samples, so that nothing has to
531
be assumed about either, the line comes out as 128 x period / refresh:
532
533
| R#9 | sprites off | sprites on |
534
|---|---|---|
535
| s = 0 | 1368.14 ± 0.00 | 1367.97 ± 0.03 |
536
| s = 16 | 1365.03 ± 0.02 | 1364.98 ± 0.03 |
537
| s = 32 | 1365.04 ± 0.03 | 1364.95 ± 0.04 |
538
| s = 48 | — | 1365.01 ± 0.10 |
539
540
1368 and 1365 exactly, as documented, in both modes. And the three cycles come off
541
precisely where section 4.4 says the padding is:
542
543
```
544
sprites off, 1368:  ... 1314 -8- 1322 -10- 1332 -10- 1342 -8- 1350 ...
545
sprites off, 1365:  ... 1314 -8- 1322  -9- 1331  -8- 1339 -8- 1347 ...
546
547
sprites on,  1368:  ... 1315 -15- 1330  -7- 1337 -11- 1348 -6- 1354 ...
548
sprites on,  1365:  ... 1315 -14- 1329  -6- 1335 -10- 1345 -6- 1351 ...
549
```
550
551
**The 1365 line does not remove all the padding.** In sprites-off the pair goes from
552
10 + 10 = 20 to 9 + 8 = 17, where the unpadded value would be 8 + 8 = 16; in sprites-on
553
the trio goes from 15 + 7 + 11 = 33 to 14 + 6 + 10 = 30, where the rest of that part of
554
the line runs 13 + 6 + 10 = 29. So a 1365-cycle line still carries one cycle of padding,
555
and the 1368-cycle line carries four — in both modes, which is what makes it four in the
556
sprites-on model as well. (Reading the difference as "the whole padding" would put the
557
sprites-on total at 3, which is what an earlier version of this document did.)
558
559
**Horizontal set-adjust moves the padding but never its total.** All 16 values of R#18 in
560
all three modes: the line stays at 1368 to ±0.1 cycles, nothing before cycle ~1290 moves,
561
and the padding block slides at 4 cycles per unit — one screen-5 pixel — with R#18 0..7
562
shifting it later and 8..15 (i.e. −8..−1) earlier. Even values keep the two-of-+2 shape,
563
odd values sit half a memory cycle away and spread the same 4 cycles as +1, +2, +1:
564
565
```
566
R#18 = 0:  1334:+2  1344:+2          R#18 = 1:  1333:+1  1343:+2  1352:+1
567
R#18 = 2:  1342:+2  1352:+2          R#18 = 3:  1341:+1  1351:+2  1360:+1
568
R#18 = 8:  1302:+2  1312:+2          R#18 = 9:  1301:+1  1311:+2  1320:+1
569
```
570
571
The total is invariant at +4, which is an independent confirmation of the paragraph above.
572
openMSX has one table per mode with the padding at its R#18 = 0 position, so a program
573
that changes the set-adjust gets the padding in the wrong place — worth at most 4 cycles
574
on a command step that crosses it, and it would cost 16 tables per mode to fix.
575
576
## 11. The command start delay, all six commands
577
578
`scr5-{mode}-{cmd}-noCpu-nx4-ny2-*h` restarts a 4x2 command in a loop, so the interval
579
between the `OUT` that commits R#46 and the command's first VRAM access is visible. Each
580
launch gives an interval: the upper bound is the distance to the access that happened, the
581
lower bound the distance to the command slot before it, which the command did not take.
582
Distances are in memory cycles; the /CSW edge is rounded up to a whole cycle, as a CPU
583
port access is.
584
585
| command | first access | display off | sprites off | sprites on | value | launches |
586
|---|---|---|---|---|---|---|
587
| LMMM | R source | 46 | 46..47 | 42..48 | **46** | 49/49 |
588
| LMMV | R dest | 70 | 70..72 | 72..73 | **70** | 46/50 |
589
| HMMM | R source | 82 | 80..83 | 83..84 | **82** | 48/50 |
590
| YMMM | R source | 82 | 83..84 | 81..82 | **82** | 52/53 |
591
| HMMV | W dest | 94 | 93..95 | 91..96 | **94** | 54/54 |
592
| LINE | R dest | 94 | 93..95 | 92..99 | **94** | 52/52 |
593
594
Display-off pins each value to a single integer, as expected from its 8-cycle slot
595
spacing; the other two modes are consistent with it but looser. The handful of launches
596
that reject the chosen value are ones whose /CSW edge fell close enough to a cycle
597
boundary to round the other way. **The startup cost does not depend on the display mode.**
598
599
### A decomposition
600
601
The four distinct values are 46, 70, 82 and 94 — all **congruent to 10 modulo 12**, i.e.
602
603
```
604
S0 = 10 + 12 * N        N = 3 (LMMM), 5 (LMMV), 6 (HMMM, YMMM), 7 (HMMV, LINE)
605
```
606
607
Both parts connect to something already measured:
608
609
* **the 10** is the constant delay between a port access reaching the pin and the
610
  arbiter acting on it, which section 8.2 measures independently from CPU accesses as
611
  9.86 ± 0.12 cycles. So the command start pays the same pin-to-arbiter delay a CPU
612
  request does.
613
* **the 12** is the step the engine's own delays are built from: five of the seven
614
  inter-access delays in section 4.3 are multiples of 12 (R → W 24, YMMM 36, HMMM and
615
  LMMM 60, LMMV 72, LINE 84). The exceptions are HMMV's 46 and LMMM's 32.
616
617
What N counts is not clear. It is not the number of accesses per unit (1, 2, 2, 2, 3, 2
618
against N = 7, 5, 6, 6, 3, 7), nor the delay of the step that would precede the first
619
access in steady state. Four distinct values all landing on the same residue mod 12 is
620
unlikely by chance — 1 in 1728 for the three after the first — so the 12-cycle step is
621
real even if what it counts is not identified.
622
623
In an emulator's port-write coordinate, add the 3 T-states from the timestamp to the /CSx
624
rising edge (18 cycles):
625
626
```
627
LMMM 64   LMMV 88   HMMM 100   YMMM 100   HMMV 112   LINE 112
628
```
629
630
openMSX currently uses `Delta::D0` for all of them, i.e. it starts every command about
631
5 to 14 display-off slots too early.
632
633
## 12. The sprite-fetch line, and what openMSX gets wrong there
634
635
`scr5-dispOff-sprOn-stop-noCmd-1f` and `scr5-sprOn-dispOff-stop-noCmd-1f` capture the
636
vertical border boundary. Sprite attributes for a display line are fetched during the
637
*previous* line, so the access pattern cannot simply follow the display area. Counting
638
what each line actually does -- burst reads (real bitmap fetches), dummy reads of 0x1FFFF,
639
and ordinary single accesses:
640
641
| line | bursts | dummies | singles | |
642
|---|---|---|---|---|
643
| last border line | 8 | 34 | 44 | sprites-on comb, bitmap fetches dummied out |
644
| display lines | 48 | 2 | 48 | |
645
| last display line | 41 | 45 | 12 | bitmap still fetched, sprite slots dummied out |
646
| first border line | 0 | 16 | 1 | display-off comb |
647
648
So the line before the display area already runs the sprites-on access pattern, with its
649
32 bitmap slots spent on dummy reads, and the last display line still fetches bitmap while
650
dummying out every sprite slot.
651
652
**Where the grid changes.** An earlier version of this section read the comb as changing at
653
the line boundary. That is wrong, and per-line calibration (each line fitted on its own
654
eight refresh pulses, which the measurement repository's `lines.py` does) shows it plainly.
655
The line before the display area:
656
657
```
658
    6-  14-  22- ...  110-  118-   162-  170-  182a  188-  194.  214a  220- ...
659
```
660
661
-- the left horizontal blanking runs the *sprites-off* comb, and from 162 on the line is
662
identical to a display line except that its bitmap slots hold dummy reads. And the line
663
after the display area:
664
665
```
666
    4.  17.  30-  36.  46. ...  116.  126.   164-  172-  180-  188- ...
667
```
668
669
-- the sprites-on sprite-fetch comb up to 126, all of it dummy, and the display-off comb
670
from 164 on. So the access grid changes **at the same point in the line at both ends**,
671
somewhere in (126, 162]: the last left-comb access and the first slot of the new grid
672
bracket it, and the two tables happen to agree at 162 and 170, so any switch point in that
673
window behaves identically.
674
675
That is a different line origin for the access grid, not a different number of lines. In
676
slots available to the command engine and the CPU:
677
678
| line | hardware | openMSX | switching one whole line early | switching at 162 |
679
|---|---|---|---|---|
680
| the line before the display area | 44 | 154 | 31 | 44 |
681
| the line after the display area | 140 | 154 | 154 | 140 |
682
683
openMSX's `getTab()` keys on `isDisplayEnabled() = isDisplayArea && displayEnabled`, so it
684
returns `tabScreenOff` for both, and gives a command 110 slots too many on the line before
685
the display area -- for one line per frame, a command there runs several times too fast.
686
Switching a whole line early is most of the fix; switching at cycle 162 of the line before
687
and of the line after is the whole of it, and is no harder, because the sync point that
688
changes the table already exists (`VDPVRAM::updateDisplayEnabled()` syncs the command
689
engine) and only its moment has to move.
690
691
One detail even that does not capture: the left blanking of the line before the display
692
area runs the sprites-off comb (6, 14, ... 118), not the display-off one (0, 8, ... 120) --
693
the same number of slots six cycles later, because that line fetches no sprite patterns for
694
the border line above it. Not worth a fourth table.
695
696
## 13. How well the lookahead and the pin delay separate
697
698
The lead from the /CSx edge to the granted slot is about 26 cycles, made of a wall-clock
699
pin delay and a lookahead counted in memory cycles. Only the lookahead sees the padding,
700
so the two are separable only on requests whose window straddles it — which is what the
701
`IN ; NOP ; NOP` loop supplies: 22 T-states is near-coprime with the 128-cycle refresh
702
cell, so one capture creeps through every phase instead of visiting two or three.
703
704
Holding the sum at 26 and scanning the split (the constant is refitted per capture each
705
time):
706
707
| lookahead | 0 | 4 | 8 | 12 | 14 | 16 | 18 | 20 | 24 | 28 |
708
|---|---|---|---|---|---|---|---|---|---|---|
709
| the 132 loop, of 1733 | 11 | 6 | 6 | **4** | **4** | **4** | 6 | 6 | 6 | 10 |
710
| all display-off, of 4888 | 104 | 93 | 93 | 75 | 75 | 75 | **68** | **68** | **68** | 83 |
711
712
So the split is real: a lookahead of 0 (all of it wall-clock, no padding seen) and one of
713
28 (all of it counted in memory cycles) are both excluded, from both directions. The
714
optimum is broad and the two corpora disagree slightly about where it sits — 12..16 on the
715
designed experiment, 18..24 on the larger display-off set, a difference of 7 accesses in
716
4888. The 2013 set is the sharp constraint: there the lookahead must be at most 16, and 17
717
already costs 10 of 1141. 16 is the value that satisfies all three, and it is what openMSX
718
already uses.
719
720
**Horizontal set-adjust is very nearly invisible to the CPU.** Moving the padding with
721
R#18 should move the CPU's granted slot for the few requests whose window straddles it.
722
Fitting the `stop-rdCpu-adjust*` captures with the padding at its R#18 = 0 position and
723
with it shifted by 4 cycles per unit: 26 versus 22 mispredictions over 1837 display-off
724
accesses, and 22 versus 19 over 1830 sprites-off ones. The shifted version is better in
725
both, which is the direction the model predicts, but seven accesses out of 3667 is not a
726
measurement.
727
728
## 14. A trap in finding the refresh chain
729
730
Everything here is anchored on the refresh accesses: they are 128 cycles apart, the row
731
address increments and the bank alternates, and the gap before the first of a line is 472.
732
Row and bank alone are not enough. In screen 5 an ordinary access can supply a false link,
733
and one false link moves the anchor by a refresh cell or, in one capture, by a whole line —
734
which is invisible in slot arithmetic, because 128 is a multiple of the slot spacing, but
735
wrong for anything that compares VRAM accesses with the /CSx pins.
736
737
Requiring a single /CAS as well (bitmap fetches are bursts) fixes it: 16 of 1051 captures
738
are anchored differently with and without that test, and the CPU model improves from
739
0.93 % to 0.75 % mispredicted.
740
741
What does *not* work is requiring a constant column address along the chain. The column
742
is not constant: it carries when the row counter wraps, e.g.
743
```
744
row 7c col 7f   row 7d col 7f   row 7e col 7f   row 7f col 7f   row 80 col bf   row 81 col bf
745
```
746
so that test breaks the chain in the middle and makes matters worse. The VDS pin would be
747
the clean discriminator if one is needed.
748
749
## 15. Reconstructed requests: how much freedom the trellis really has
750
751
The residual of section 8 -- 175 of 23353 CPU accesses mispredicted -- is not a model
752
error. It is what is left of the uncertainty in *when* each request was made: `/CSx` is
753
sampled at 80 MHz, the time base is interpolated between refresh cycles, and the arbiter
754
acts on an integer VDP cycle, so an edge that falls near a cycle boundary can arm either
755
side of it. Section 13 removes most of that by fitting the Z80's own instruction grid
756
rather than each edge; the measurement repository removes the rest with a Viterbi-style
757
search over the per-edge assignments (`TRELLIS_CPU_REQUESTS.md`).
758
759
Such a search deserves to be treated with suspicion: enough per-edge freedom will fit
760
anything. These are the numbers that say it does not have enough.
761
762
**What it uses.** One real pin delay `phi` shared by every edge of every capture, plus a
763
per-edge tolerance `eps` that the search minimises. Over the 266 captures and 24597
764
recorded `/CSx` edges:
765
766
| `eps` | in analyzer samples | captures reconstructed exactly | edges moved | one shared `phi` |
767
|--|--|--|--|--|
768
| 0 | 0.00 | 174 / 266 | 0 / 24597 | 114 / 266 |
769
| 0.05 | 0.19 | 195 / 266 | 30 | 147 / 266 |
770
| 0.134 | 0.50 | 220 / 266 | 83 | 190 / 266 |
771
| 0.268 | 1.00 | 255 / 266 | 151 | 224 / 266 |
772
| 0.5 | 1.86 | 264 / 266 | 142 | 263 / 266 |
773
774
Each row is one run with `eps` held at that value. With *no* per-edge freedom at all, one
775
real number per capture already reproduces every grant in two thirds of the corpus. The
776
published run instead takes the smallest `eps` that works for each capture: 174 of the 264
777
need none at all, 78 need up to one analyzer sample, 10 up to one and a half, and 2 need
778
1.86 -- and over the whole corpus 242 of the 24597 edges (0.98 %) move. The shared `phi`
779
that covers all three modes is confined to `[10.746, 10.748)` -- two thousandths of a
780
cycle out of the one-cycle period it could have taken.
781
782
The tail is the one place where the published figure leans on more slack than the stated
783
one-sample uncertainty: twelve captures need up to 1.86 samples. That is not unreasonable
784
-- the sampling step is not the only error, since the interpolated time base contributes
785
too -- but the 263 of 266 should be read next to the 224 that a tolerance of exactly one
786
sample gives.
787
788
**That it discriminates.** The stronger evidence is that wrong models fail. Re-running the
789
same search with one part of the model perturbed, and counting captures reconstructed with
790
one shared `phi`:
791
792
| variant | exact | one shared `phi` | `phi` band |
793
|--|--|--|--|
794
| the model as published | 264 / 266 | **263 / 266** | 0.002 wide |
795
| no sub-slot classes (`NEED` 16/18 by mode only) | 242 | 170 | 0.002 |
796
| sub-slot classes assigned at random, same count (3 draws) | 174..183 | 126..134 | 0.02..0.16 |
797
| lookahead 15 instead of 16 | 246 | 227 | 0.030 |
798
| lookahead 17 instead of 16 | 263 | 255 | 0.014 |
799
| no threshold classes (uniform release margin) | 258 | 246 | 0.037 |
800
| release margin 0 instead of 1 | 259 | 254 | 0.002 |
801
| two-entry request buffer | 209 | **208** | 0.411 |
802
| padding as four single cycles (the derived positions) | 261 | 247 | 0.002 |
803
804
The permutation control is the decisive one: sub-slot classes of exactly the same size,
805
drawn at random, take sprites-off from 83 of 83 captures down to 15..22. So the search
806
cannot absorb an arbitrary lattice; it is testing the one that was derived from the die.
807
The two-entry buffer is excluded as well, which is worth noting because the buffer depth
808
was the part of the model I had said rested on fitting alone.
809
810
**What it cannot see.** Three parts of the model are invisible to this fit, and its score
811
should not be read as evidence for them:
812
813
* the packed-slot dummy read (`-19` versus `-18`). The CPU is never served in a packed
814
  slot in either formulation, so the boundary does not enter the grant sequence at all;
815
  changing it moves nothing. The dummy-read census of section 6 remains the only
816
  measurement that bears on it.
817
* the sprites-on addend of the command engine: 0, 1 and 2 all score identically here.
818
* the release margin 1 versus 2, which score identically.
819
820
**Independent replay.** The reconstruction is published as one `.cpureq` file of integer
821
request cycles per capture. Feeding those through the arbiter of
822
`cpu-arbitration-model.py` -- no fitting anywhere, the request times come from the
823
reconstruction and the accesses from the `.txt` -- gives:
824
825
| | captures exact | accesses | not predicted | spurious |
826
|--|--|--|--|--|
827
| display off | 100 / 100 | 9529 | **0** | 0 |
828
| sprites off | 79 / 83 | 7926 | **0** | 4 |
829
| sprites on | 60 / 64 | 4924 | **0** | 4 |
830
831
Every one of the 22379 observed CPU accesses is reproduced. The eight spurious grants are
832
one access each in eight captures: four are the same captures the repository's own replay
833
fails on (its checked-in fixtures predate its final threshold formulation), and the other
834
four flip to the other side if the release margin is 2 rather than 1 -- the parameter the
835
corpus does not settle. So the 0.75 % of section 8 was the reconstruction of the request
836
times, not the arbiter.
837
838
The same replay run with a *uniform* release margin -- what openMSX did before the slot
839
classes were implemented -- loses 31 of the 4924 sprites-on accesses at margin 1 and 67 at
840
margin 2, and none at all with the display off or sprites off.
841
842
## 16. What of this is now in openMSX
843
844
| rule | where |
845
|--|--|
846
| the three slot tables | `slotsScreenOff` / `slotsSpritesOff` / `slotsSpritesOn` (unchanged) |
847
| line padding, 4 cycles in measured positions per mode | `Timing::pad` |
848
| command steps counted in memory cycles | `CycleTable`, `dist()` |
849
| the sprites-on addend on every command step | `Timing::extra` |
850
| a step that starts in a tight slot costs one cycle more | `CycleTable`, post-pass |
851
| the CPU is never served in a tight slot | `CycleTable`, `ok()` |
852
| the CPU's lookahead counted in memory cycles | `CycleTable`, `ok()` |
853
| 18 cycles of lookahead for a 'late' slot | `Timing::late` / `Timing::plain` |
854
| the request is registered 29 cycles after the port access | `VDP::scheduleV99x8VramAccess` |
855
| one request buffer, released `threshold` cycles before its access | `VDP::scheduleV99x8VramAccess` |
856
| the command start delay, per command | `Delta::CMD_START_*` |
857
858
The generated tables were checked entry by entry against the model in this directory:
859
103968 table entries over the four V99x8 tables, 304 slot thresholds and 165424
860
memory-cycle distances all agree exactly.
861
862
Not emulated, and why:
863
864
* **the packed-slot dummy read.** openMSX keeps the CPU out of a tight slot, which is the
865
  visible half. The dummy read itself only shows up as a command slot the engine loses,
866
  in a 3-cycle-wide request window.
867
* **R#9 S1/S0 and R#18.** Both need a second set of tables per mode; R#18 needs sixteen.
868
  Worth at most 4 cycles on a step that crosses the padding.
869
* **the border/display switch one line early.** Needs the table to change inside a line.
870
* **text, character and MSX1 tables.** Only the G1/G2/G3 CPU slot classes are known; the
871
  step delays for those modes have never been measured.
872
* **the two clocks are asynchronous on real hardware** (5.96113 VDP cycles per T-state on
873
  the NMS 8280, section 8). openMSX runs them at exactly 6, so a request always lands on
874
  the same phase. Nothing to do about that here.
875
876
## 17. The command start for the other six commands
877
878
The `*i.vcd` captures add POINT, PSET, SRCH, LMCM, LMMC and HMMC, display off, fifteen
879
captures each. Same method as section 11 -- the last `/CSW` of the register burst is the
880
write to R#46, and the slot the first VRAM access lands in, against the slot before it that
881
the command did not take, brackets the delay -- but with one improvement that matters here.
882
883
Rounding each launch's `/CSW` edge to a whole cycle on its own makes a launch whose edge
884
sits close to a cycle boundary round the wrong way, which is what left the earlier six with
885
one or two dissenting launches each. Instead, all 328 launches of all twelve commands were
886
fitted with **one shared real pin delay**: the arbiter sees the write at
887
`floor(t_csw + phi)`, with the same `phi` for every launch, which is the constant the CPU's
888
own requests pay. That has a clear optimum -- 4 of 328 launches unsatisfied at the best
889
offset against 15 at the worst -- and at it every command pins to a single integer:
890
891
| command | `S0` from `/CSW` | from the port-write timestamp | first access | launches |
892
|---|---|---|---|---|
893
| POINT | **45** | 63 | R source | 26/26 |
894
| LMMM | **46** | 64 | R source | 21/21 |
895
| LMCM | **58** | 76 | R source | 28/28 |
896
| LMMV | **70** | 88 | R dest | 25/28 |
897
| LMMC | **70** | 88 | R dest | 29/29 |
898
| PSET | **70** | 88 | R dest | 28/28 |
899
| SRCH | **70** | 88 | R source | 27/28 |
900
| HMMM | **82** | 100 | R source | 27/27 |
901
| YMMM | **82** | 100 | R source | 29/29 |
902
| HMMV | **94** | 112 | W dest | 28/28 |
903
| LINE | **94** | 112 | R dest | 28/28 |
904
| HMMC | **94** | 112 | W dest | 28/28 |
905
906
Eleven of the twelve are `46 + 12k`, so section 11's `S0 = 10 + 12N` survives with
907
N = 3, 4, 5, 6, 7. **POINT is the exception**: 45, one cycle below LMMM, whose first access
908
is the same source read. It is not a rounding artefact -- at the fitted `phi` the admissible
909
band for POINT is the single value 45 over all 26 launches, and 46 is outside it.
910
911
The groupings are suggestive and still unexplained. The value does not follow the operand
912
(SRCH reads its source and sits with the three destination-read commands), nor the number of
913
accesses per unit, nor the byte/pixel distinction, nor whether the command writes at all
914
(POINT and SRCH both only ever read, and they are 25 cycles apart).
915
916
For LMCM, LMMC and HMMC this is the startup only. Their transfer timing is a separate
917
matter: openMSX performs the transfer access at the moment the byte arrives rather than at
918
the next slot, which is a TODO that predates this work.
919
920
## 18. Character and text modes
921
922
The `*i.vcd` set also adds character and text captures: `noCpu` with R#9 S1/S0 = 0 and = 16,
923
and `wrCpu` at 72 cycles, for screens 1, 2, 3 and 4 and for both text widths, each with the
924
display off, sprites off and sprites on.
925
926
Text mode has no refresh *chain*: `vdp-timing-2.html` measured that the text modes refresh
927
differently, seven reads per line instead of eight and clustered at cycles 74 to 122 rather
928
than spread over the line, so the row-incrementing 128-cycle chain the other modes give is
929
not there. Two anchors are available instead, and they agree:
930
931
* fold each capture on its own line marks, which leaves one unknown -- the offset of that
932
  mark from cycle 0 -- and fit it. That is a one-parameter search and it is sharp;
933
* or take the documented cluster directly: seven single accesses eight cycles apart, the
934
  first at cycle 74.
935
936
The captures confirm the cluster exactly -- 74, 82, 90, 98, 106, 114, 122, then the two
937
dummy reads at 230 and 238 and the rendering groups from 246 in steps of 48, each
938
`g, g+18, g+24, g+30, g+36, g+42` -- which is `vdp-timing-2.html` reproduced twelve years
939
later on a different machine. Anchoring on the cluster instead of on the fitted mark puts
940
90 to 95 % of the CPU writes on a `slotsText` slot and uses 46 of the 47, against the fit's
941
100 % and 45: the same answer, the difference being that a run of CPU writes in the
942
eight-apart slots can impersonate the cluster.
943
944
**The slot tables are confirmed, including the text phase.** Folding every CPU write onto
945
the line and sweeping the offset:
946
947
| capture set | writes | on `slotsChar` | on `slotsText` | on the display-off table | slots used |
948
|---|---|---|---|---|---|
949
| screen 2, display off | 636 | **100 %** | 6 % | 94 % | 30/31 |
950
| screen 2, sprites off | 626 | **100 %** | 6 % | 94 % | 30/31 |
951
| screen 2, sprites on | 635 | **100 %** | 5 % | 95 % | 30/31 |
952
| text 40, display off | 533 | 7 % | **100 %** | 0 % | 45/47 |
953
| text 40, sprites off | 179 | 6 % | **99 %** | 0 % | 42/47 |
954
| text 40, sprites on | 272 | 7 % | **100 %** | 0 % | 45/47 |
955
| text 80, display off | 549 | 7 % | **100 %** | 0 % | 45/47 |
956
| text 80, sprites off | 208 | 8 % | **100 %** | 0 % | 45/47 |
957
| text 80, sprites on | 267 | 7 % | **100 %** | 0 % | 44/47 |
958
959
The best offset is **166 in every one of the nine sets** -- the line mark sits at cycle 166,
960
which is the first slot after the widest gap in both tables, so the fit is self-consistent
961
as well as sharp. Pooled, 1896 of 1897 character-mode writes land on a `slotsChar` slot and
962
all 31 slots are used; 2008 of 2010 text writes land on a `slotsText` slot and 45 of the 47
963
are used (174 and 182 are never reached by a 72-cycle request train).
964
965
That settles several things at once:
966
967
* **`slotsText` is right as it stands, phase included.** The circuit model of the
968
  measurement repository confirms the 47-slot cyclic structure but keeps a global
969
  one-`phiL` phase ambiguity; this fixes the phase, and it is the one openMSX already has.
970
* **TEXT1 and TEXT2 share the table**, as openMSX assumes.
971
* **The character table does not change when sprites are disabled.** `vdp-timing-2.html`
972
  lists this as unmeasured and guesses that the sprite bandwidth becomes available to the
973
  CPU as it does in bitmap modes. It does not: sprites off scores exactly the same 100 % on
974
  the unchanged table.
975
* **Nor when the display is disabled**, in either character or text mode. For text that
976
  answers the other open question in `vdp-timing-2.html`: screen-off in text mode is *not*
977
  the common screen-off schema, it is the text schema (0 % of text writes land on the
978
  display-off table). Both confirm what openMSX does after
979
  [issue 1754]https://github.com/openMSX/openMSX/issues/1754.
980
981
**Screen 4 has the same lattice as screens 1, 2 and 3.** Their line-end combs are identical
982
except that screen 4 adds accesses at 1255 and 1319 -- the sprite-mode-2 attribute fetches
983
-- and both fall in gaps that are not CPU slots in any of them. That is the third
984
`vdp-timing-2.html` guess confirmed.
985
986
**R#9 S1/S0 shortens the line in these modes too.** The raw `/RAS` line period needs no
987
absolute scale to compare, and the ratio is the same everywhere:
988
989
| mode | screen 1 | screen 2 | screen 3 | screen 4 | text 40 | text 80 |
990
|---|---|---|---|---|---|---|
991
| line at S1/S0 = 16 | 1365.04 | 1365.01 | 1364.94 | 1365.05 | 1364.96 | 1365.02 |
992
993
In the character modes the positions are measurable, and they are not where the bitmap ones
994
are. Comparing the two settings:
995
996
```
997
S1/S0 = 0    ... 1300 -6- 1306 -35- 1341 -11- 1352 -6- 1358 -10- 1368
998
S1/S0 = 16   ... 1300 -6- 1306 -33- 1339 -10- 1349 -6- 1355 -10- 1365
999
```
1000
1001
so **+2 completing at 1341 and +1 at 1352**, identical in screens 1, 2, 3 and 4. As in the
1002
bitmap modes the two settings differ by 3 while the total is probably 4, and the fourth
1003
cycle cannot be located without an unpadded reference. Not implemented: `tabChar` has no
1004
padding today, and the CPU's lookahead is the only thing in those modes that would notice.
1005
1006
For text the positions are narrowed but not pinned. Anchored on the refresh cluster, every
1007
access from 74 through the last rendering read at 1200 sits at an **identical** position in
1008
both settings, so all three cycles are removed between cycle 1200 and the next line's
1009
cluster at 74 -- the right blanking region, which is where the bitmap and character ones
1010
are too. Pinning them further needs an access inside that region: the CPU slots at 1206 to
1011
1362 are there, so a text `wrCpu` capture with S1/S0 = 16 would do it, where the `noCpu`
1012
captures that exist cannot.