wdbcvt: reading Vivado’s undocumented .wdb waveform files
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.mdin 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,mainis 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/latexin 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:

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.
