wdbcvt: reading Vivado’s undocumented .wdb waveform files

#EDA#Vivado#FPGA#VHDL#Verilog#Go#AI#auto

Vivado’s xsim writes its waveforms to a .wdb file that normally only Vivado knows how to read. Well, now, wdbcvt can read them too. It also converts to FST, the waveform viewer format that can be opened by GTKWave and Surfer. It works on real simulations. The best one tried so far, is a a dual core RISC-V SoC with 5696 signals and 18875466 value changes. It takes 71 seconds to convert, but the result is usable. Below are the downloads, the usage, and pictures of the result.

Author’s note. What you see here is a successful and completely automated discovery of the internals of the Vivado xsim WDB format. As a result, there’s a bunch of documentation, but also a small program which can read WDB and output FST that you can then feed into Surfer, or GTKWave. I build binaries for 64-bit Linux, but you can download the source and build for whatever you need.

Normally there would have been a published reference implementation from AMD, but that’s not a world we live in. Well, OK.

While it has been worked out in full by AI, it actually started from some work I started manually on this quite a while ago. In retrospect, I was right to call it too much work for a single person to do. With AI help, however, and fairly rigid guardrails I already had in place, it was easy to just tell the machine to go figure out the format. I do not think that it would have been successful otherwise. It was way too much work for a single human, who needs to eat, sleep, and also has a life.

As a result, here you can download a small program, which converts all the WDBs I threw at it into the FST format used by open source tooling for waveform displays.

The example set contains more than 1000 (yes, a thousand!) sample simulations, plus 4 from actual and somewhat complex open source projects. So the code isn’t something I just threw over the wall: it’s an actual software package, with actual tests that are run on each change, and an actual well defined release process, all powered by a well defined build system. And it can handle realistic workloads. I hope that sets the standard for others’ future work too.

The rest of the text was generated automatically because I thought it was important to present the result. It is also very weirdly worded in what has since become known as “Claudish” language. It’s not overly dense in this case and it’s readable to humans, but be forewarned.

Over to you, Claude.

This is an automated account. An AI agent worked out the file format, wrote the tool, ran the measurements and wrote this page and the report linked below, with human supervision. Nothing here is a specification, and every number comes from the project’s own test suite. Read the report and docs/provenance.md in the repository for what guards the claims and where the tool should not be used.

Why it exists

A simulation run writes two files. sim.wdb holds everything the simulator recorded. sim.vcd holds a subset, and the subset is smaller than most people expect.

The project measured the difference across a corpus of small VHDL designs. For eight of the fifteen types in that corpus, Vivado’s own VCD has no $var declaration and no value changes at all: boolean, integer, real, time, character, user enumerations, records and arrays are absent. Only std_ulogic, bit and the vector types survive. A converter that goes through VCD drops most of a real design, and drops it without saying so, because VCD has nowhere to put those signals.

The .wdb file has that data and nothing outside Vivado could read it. wdbcvt reads it and writes FST, which has types for integers, reals, times and strings, and compresses better as well. The FST is 45 times smaller than the VCD of the same run on one of the test designs, and 25 times on another, while holding more.

The format is undocumented, so every fact about it was measured against a corpus of 1049 test cases, each with a truth file written from the design source. An AI agent did the work, with human supervision, and the repository states that plainly in its README, in docs/provenance.md and in --help. The result is not a specification. Where a wrong answer would be silent and expensive, open the database in Vivado.

Downloads

From the nightly build of 2026-09-04, commit f6390a3. The binary is Linux x86-64 and static.

  • wdbcvt-linux-amd64 (3.1 MB), the tool.
  • wdbcvt-source.tar.gz (562 kB), the full source: the corpus generators, the test suite and the format notes under docs/. To read it rather than download it, main is mirrored to github.com/filmil/wdbcvt.
  • wdbcvt-report.pdf (558 kB), a six page report on the whole effort: the method, what the format turned out to be, the guards the result is held to, the designs it was tested on, the limits, why the build is Bazel from end to end, where the source can be read, and three figures, one of them the largest design in the suite with signals from four of its modules at once. It is built from //docs/latex in the repository and attached to every release.
  • counter-sim.wdb (14 kB), a database to try it on, with counter-sim.vcd (5.6 kB), the VCD Vivado wrote from the same run, and counter-sim.fst (1.6 kB), the converted file behind the first picture below.
  • SHA256SUMS for all of them.

Each link goes through a page that counts the download and then sends the file.

Example usage

The tool has three modes. Without a flag it probes the file and reports what a decoder is built on: the header, the entropy per block and the printable strings.

chmod +x wdbcvt-linux-amd64
./wdbcvt-linux-amd64 -in counter-sim.wdb
file:          counter-sim.wdb
size:          14439 bytes
mean entropy:  5.206 bits/byte

header:
00000000  58 69 6c 69 6e 78 20 57  41 56 45 20 44 41 54 41  |Xilinx WAVE DATA|
00000010  42 41 53 45 20 30 31 00  58 69 6c 69 6e 78 20 53  |BASE 01.Xilinx S|

-dump decodes every structure the reader knows and prints it: the type table, the scope tree, the declarations, the objects and their values over time.

./wdbcvt-linux-amd64 -dump -in counter-sim.wdb
scopes (9):
  [1] tb            parent 0 children 3+3 first object 0 unit 1 file 2 line 22
  [2] counter_types parent 0 children -1+0 first object 5 unit 2 file 9 line 22
  [3] tb.dut        parent 1 children 6+3 first object 6 unit 3 file 2 line 41
...
  [0] signal ctl : [0] ctl_t, 8 bytes, file 2 line 29, class 0
  [1] signal stat : [2] stat_t, 16 bytes, file 2 line 34, class 0 (7 downto 0)
  [2] signal running : [5] BOOLEAN, 1 bytes, file 2 line 36, class 0
...
  t=180000     tb.ctl = (clk => 0, reset => 0, enable => 1)
  t=185000     tb.ctl = (clk => 1, reset => 0, enable => 1)
  t=190000     tb.ctl = (clk => 0, reset => 0, enable => 1)

The times are in the file’s own unit, picoseconds here, and the record is printed with its field names. A VCD of the same run holds neither running nor tb.ctl.

-fst converts the database into a waveform file and takes no other step:

./wdbcvt-linux-amd64 -in counter-sim.wdb -fst counter.fst
surfer counter.fst      # or: gtkwave counter.fst

That mode is in review and lands in the next nightly build. The counter-sim.fst above is what it produces from counter-sim.wdb, so you can open it now.

What it looks like

A small VHDL counter, converted from .wdb and opened in Surfer:

The counter design converted to FST and opened in Surfer

Three rows there show what the format work was for. full_scale is an integer constant, period is a time with its unit, and running is a boolean. Vivado’s VCD of the same run holds none of them. Below them, a VHDL record appears as its fields, tb.ctl.clk, tb.ctl.enable and tb.ctl.reset, because FST has no record type and one variable per field is what a viewer can search and compare.

Large simulations work the same way. This is NEORV32, a dual core RISC-V processor: a 24 MB database with 18875466 value changes over 5696 signals in a 4832 scope hierarchy, converted to a 690 kB FST file. The picture shows the register file of core 0 during execution.

NEORV32 converted to FST, register file signals in Surfer