|
| 1 | +- Start Date: 2025-05-12 |
| 2 | +- RFC PR: [amaranth-lang/rfcs#0074](https://github.com/amaranth-lang/rfcs/pull/0074) |
| 3 | +- Amaranth Issue: [amaranth-lang/amaranth#1599](https://github.com/amaranth-lang/amaranth/issues/1599) |
| 4 | + |
| 5 | +# Structured VCD Output |
| 6 | + |
| 7 | +## Summary |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +When generating VCD files, use the `scope` keyword to distinguish aggregate signals. |
| 11 | + |
| 12 | +## Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +These changes are intended to make it easier for waveform viewers to recover information about aggregate signals from VCD files. |
| 16 | +This is partially motivated by an issue with VCD parsing that occurs in Surfer (see [ekiwi/wellen#36](https://github.com/ekiwi/wellen/issues/36)). |
| 17 | +Ideally, this leads to a better user experience when inspecting simulated Amaranth designs with a waveform viewer. |
| 18 | + |
| 19 | +## Guide-level explanation |
| 20 | +[guide-level-explanation]: #guide-level-explanation |
| 21 | + |
| 22 | +This RFC describes changes to the way Amaranth generates a VCD file. |
| 23 | +When reading a VCD file, waveform viewers like [Surfer](https://surfer-project.org/) and [GTKWave](https://gtkwave.sourceforge.net/) support use of the `scope` keyword for organizing variables into groups. |
| 24 | +Currently, when writing a VCD file, Amaranth uses the `module` scope to distinguish between signals belonging to different modules. |
| 25 | +For instance: |
| 26 | + |
| 27 | +``` |
| 28 | +$scope module top $end # top |
| 29 | + $var wire 1 <id> clk $end # top.clk |
| 30 | + $var wire 1 <id> rst $end # top.rst |
| 31 | + $var wire 32 <id> in $end # top.in |
| 32 | + $var wire 32 <id> out $end # top.out |
| 33 | + $scope module submodule $end # top.submodule |
| 34 | + $var wire 1 <id> clk $end # top.submodule.clk |
| 35 | + $var wire 1 <id> rst $end # top.submodule.rst |
| 36 | + $var wire 5 <id> in $end # top.submodule.in |
| 37 | + $var wire 5 <id> out $end # top.submodule.out |
| 38 | + $upscope $end |
| 39 | +$upscope $end |
| 40 | +``` |
| 41 | + |
| 42 | +However, it does *not* attempt to use scopes for organizing the members of aggregate signals with array-like or struct-like datatypes. |
| 43 | +Instead, when creating VCD variables for aggregate signals, members are distinguished only by appending to the name of the parent signal. |
| 44 | +For instance, a signal `top.submodule.my_array` with the type `ArrayLayout(unsigned(32), 4)` is currently represented as: |
| 45 | + |
| 46 | +``` |
| 47 | +$scope module top $end # top |
| 48 | + ... |
| 49 | + $scope module submodule $end # top.submodule |
| 50 | + ... |
| 51 | + $var wire 128 <id> my_array $end # top.submodule.my_array |
| 52 | + $var wire 32 <id> my_array[0] $end # top.submodule.my_array[0] |
| 53 | + $var wire 32 <id> my_array[1] $end # top.submodule.my_array[1] |
| 54 | + $var wire 32 <id> my_array[2] $end # top.submodule.my_array[2] |
| 55 | + $var wire 32 <id> my_array[3] $end # top.submodule.my_array[3] |
| 56 | + $upscope $end |
| 57 | +$upscope $end |
| 58 | +``` |
| 59 | + |
| 60 | +This is not sufficient for explicitly conveying the relationship between aggregate signals and their members to a waveform viewer. |
| 61 | +In this case, `my_array` does not explicitly *contain* its members, and waveform viewers may only *infer* this relationship by attempting to recover it from the names. |
| 62 | + |
| 63 | +This RFC proposes the use of `scope vhdl_array` and `scope vhdl_record` to pass information about these relationships to a waveform viewer. |
| 64 | +After this change, the VCD output above would become: |
| 65 | + |
| 66 | +``` |
| 67 | +$scope module top $end # top |
| 68 | + ... |
| 69 | + $scope module submodule $end # top.submodule |
| 70 | + ... |
| 71 | + $comment Flattened representation of 'my_array' $end |
| 72 | + $var wire 128 <id> my_array $end # top.submodule.my_array |
| 73 | +
|
| 74 | + $comment Hierarchical representation of 'my_array' members $end |
| 75 | + $scope vhdl_array my_array $end # top.submodule.my_array |
| 76 | + $var wire 32 <id> 0 $end # top.submodule.my_array.0 |
| 77 | + $var wire 32 <id> 1 $end # top.submodule.my_array.1 |
| 78 | + $var wire 32 <id> 2 $end # top.submodule.my_array.2 |
| 79 | + $var wire 32 <id> 3 $end # top.submodule.my_array.3 |
| 80 | + $upscope $end |
| 81 | +
|
| 82 | + $upscope $end |
| 83 | +$upscope $end |
| 84 | +``` |
| 85 | + |
| 86 | +These images demonstrate the difference in GTKWave output: |
| 87 | + |
| 88 | +<figure> |
| 89 | + <img align="center" src="./0074-structured-vcd/gtkwave_pre_rfc.png" alt="a screenshot of array-like signals in GTKWave (current behavior)"> |
| 90 | + <figcaption><i>Figure 1. A screenshot of array-like signals in GTKWave (current behavior)</i></figcaption> |
| 91 | +</figure> |
| 92 | + |
| 93 | +<figure> |
| 94 | +<img align="center" src="./0074-structured-vcd/gtkwave_post_rfc.png" alt="a screenshot of array-like signals in GTKWave (proposed behavior)"> |
| 95 | + <figcaption><i>Figure 2. A screenshot of array-like signals in GTKWave (proposed behavior)</i></figcaption> |
| 96 | +</figure> |
| 97 | + |
| 98 | +... and the difference in Surfer output: |
| 99 | + |
| 100 | +<figure> |
| 101 | + <img align="center" src="./0074-structured-vcd/surfer_pre_rfc.png" alt="a screenshot of array-like signals in Surfer (current behavior)"> |
| 102 | + <figcaption><i>Figure 3. A screenshot of array-like signals in Surfer (current behavior)</i></figcaption> |
| 103 | +</figure> |
| 104 | + |
| 105 | +<figure> |
| 106 | +<img align="center" src="./0074-structured-vcd/surfer_post_rfc.png" alt="a screenshot of array-like signals in Surfer (proposed behavior)"> |
| 107 | + <figcaption><i>Figure 4. A screenshot of array-like signals in Surfer (proposed behavior)</i></figcaption> |
| 108 | +</figure> |
| 109 | + |
| 110 | +## Reference-level explanation |
| 111 | +[reference-level-explanation]: #reference-level-explanation |
| 112 | + |
| 113 | +Currently, Amaranth represents aggregate signals in the VCD by appending the names of elements/members to the name of the signal. |
| 114 | +When simulator output is written to a VCD file, we intend that signals with aggregate datatypes are explicitly given their own scope and split into multiple VCD variables. |
| 115 | + |
| 116 | +### Changes to VCD Writing |
| 117 | + |
| 118 | +We propose the following conventions: |
| 119 | + |
| 120 | +- The `module` scope defines a group of signals belonging to the same `Module` |
| 121 | +- The `vhdl_array` scope defines a group of signals belonging to an array (such as a signal with `ArrayLayout`) |
| 122 | +- The `vhdl_record` scope defines a structured group of signals (such as a signal with `StructLayout`, `UnionLayout`, or `FlexibleLayout`) |
| 123 | + |
| 124 | +After these changes, this functionality will be enabled by default in the VCD writer. |
| 125 | +These changes will **not** be backported to Amaranth 0.5.x. |
| 126 | +Some users may require compatibility with output that is closer to the actual VCD specification. |
| 127 | +In this case, users that need the pre-RFC behavior are expected to either: |
| 128 | + |
| 129 | +- Opt-out on a case-by-case basis by passing arguments to `write_vcd()` (ie. `pure=True`) |
| 130 | +- Opt-out globally by setting an environment variable (ie. `AMARANTH_PURE_VCD=1`) |
| 131 | + |
| 132 | +### Changes to `pyvcd` |
| 133 | + |
| 134 | +The VCD writer in Amaranth depends on `pyvcd`, which represents VCD scopes with a `ScopeType` enum. |
| 135 | +`vhdl_record` and `vhdl_array` variants are required to implement this RFC. |
| 136 | + |
| 137 | +### Changes to Amaranth Playground |
| 138 | + |
| 139 | +The Amaranth Playground has a waveform viewer that depends on the [`d3-wave`](https://github.com/Nic30/d3-wave) library. |
| 140 | +`d3-wave` has an exported/extensible `RowRendererBits` class that can be used to implement renderers for particular datatypes. |
| 141 | +Custom renderers for aggregate datatypes are required for rendering grouped signals in the Amaranth Playground. |
| 142 | + |
| 143 | +### Expected VCD Output: Array-like |
| 144 | + |
| 145 | +```python |
| 146 | +from amaranth import * |
| 147 | +from amaranth.lib.data import * |
| 148 | + |
| 149 | +foo = Signal(ArrayLayout(unsigned(32), 4)) |
| 150 | +``` |
| 151 | + |
| 152 | +For `ArrayLayout`, each element in the array should become a VCD variable whose name is integer array index. |
| 153 | +In this case, the signal `foo` is split into `foo.0`, `foo.1`, `foo.2`, and `foo.3`: |
| 154 | + |
| 155 | +``` |
| 156 | +$scope vhdl_array foo $end |
| 157 | + $var wire 32 <id> 0 $end |
| 158 | + $var wire 32 <id> 1 $end |
| 159 | + $var wire 32 <id> 2 $end |
| 160 | + $var wire 32 <id> 3 $end |
| 161 | +$upscope $end |
| 162 | +``` |
| 163 | + |
| 164 | +### Expected VCD Output: Struct-like |
| 165 | + |
| 166 | +```python |
| 167 | +from amaranth import * |
| 168 | +from amaranth.lib.data import * |
| 169 | + |
| 170 | +foo = Signal(StructLayout({ |
| 171 | + "a": unsigned(1), |
| 172 | + "b": unsigned(4), |
| 173 | + "c": unsigned(32), |
| 174 | +})) |
| 175 | +``` |
| 176 | + |
| 177 | +For `StructLayout`, each member should become a VCD variable with the member name. |
| 178 | +In this case, the signal `bar` is split into `bar.a`, `bar.b`, and `bar.c`: |
| 179 | + |
| 180 | +``` |
| 181 | +$scope vhdl_record bar $end |
| 182 | + $var wire 1 id a $end |
| 183 | + $var wire 4 id b $end |
| 184 | + $var wire 32 id c $end |
| 185 | +$upscope $end |
| 186 | +``` |
| 187 | + |
| 188 | +### Expected VCD Output: Nested Aggregates |
| 189 | + |
| 190 | +```python |
| 191 | +from amaranth import * |
| 192 | +from amaranth.lib.data import * |
| 193 | + |
| 194 | +class MyStruct(Struct): |
| 195 | + a: unsigned(1) |
| 196 | + b: unsigned(4) |
| 197 | + c: ArrayLayout(unsigned(32), 4), |
| 198 | + |
| 199 | +# A struct-like signal |
| 200 | +foo = Signal(MyStruct) |
| 201 | + |
| 202 | +``` |
| 203 | + |
| 204 | +For signals with nested aggregate types, the scopes are nested in the same way that `scope module` is already used for nested modules. |
| 205 | +In this case, the signal `foo` is split like this: |
| 206 | + |
| 207 | +``` |
| 208 | +$scope vhdl_record bar $end |
| 209 | + $var wire 1 id a $end |
| 210 | + $var wire 4 id b $end |
| 211 | + $scope vhdl_array c $end |
| 212 | + $var wire 32 id 0 $end |
| 213 | + $var wire 32 id 1 $end |
| 214 | + $var wire 32 id 2 $end |
| 215 | + $var wire 32 id 3 $end |
| 216 | + $upscope $end |
| 217 | +$upscope $end |
| 218 | +``` |
| 219 | + |
| 220 | +## Drawbacks |
| 221 | +[drawbacks]: #drawbacks |
| 222 | + |
| 223 | +- VCD is not a particularly efficient format, and this adds even more bytes to generated VCD files. |
| 224 | + |
| 225 | +- This breaks any existing compatibility with waveform viewers that do not handle the `vhdl_record` and `vhdl_array` scope types. |
| 226 | + Since these are not part of the VCD specification, some waveform viewers may fail to handle this. |
| 227 | + |
| 228 | +## Rationale and alternatives |
| 229 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 230 | + |
| 231 | +- The choice of `scope vhdl_array` and `scope vhdl_record` are suggested due to known compatibility with Surfer and GTKWave. |
| 232 | + However, note that these are not part of the VCD specification (which is somewhat old and under-defined). |
| 233 | + |
| 234 | +### Alternatives |
| 235 | + |
| 236 | +- Expose an environment variable (or a parameter in `write_vcd()`) allowing users to opt-in/opt-out of structured VCD output |
| 237 | +- Include support for a different waveform format that has better-defined support for variables with composite datatypes. |
| 238 | + |
| 239 | +## Prior art |
| 240 | +[prior-art]: #prior-art |
| 241 | + |
| 242 | +- As far as I can tell, use of the `vhdl_record` and `vhdl_array` scope types comes from the [ghdl/ghdl](https://github.com/ghdl/ghdl) simulator. |
| 243 | +- [verilator/verilator](https://github.com/verilator/verilator) makes no attempt to use these scope types |
| 244 | +- [steveicarus/iverilog](https://github.com/steveicarus/iverilog) makes no attempt to use these scopes types |
| 245 | + |
| 246 | +## Unresolved questions |
| 247 | +[unresolved-questions]: #unresolved-questions |
| 248 | + |
| 249 | +None. |
| 250 | + |
| 251 | +## Future possibilities |
| 252 | +[future-possibilities]: #future-possibilities |
| 253 | + |
| 254 | +Since this adds more overhead to VCD output, it's worth mentioning that VCD may be unsuitable for testing very large designs. |
| 255 | +This RFC can also serve as a stepping stone for supporting alternative waveform formats in the future. |
| 256 | + |
0 commit comments