Skip to content

Commit ba22599

Browse files
Copilotteqdruid
andauthored
[ESI][Runtime][Cosim] Switch Verilator backend to call verilator_bin directly (#9822)
Replace the call to the `verilator` Perl wrapper with a direct call to `verilator_bin`. The C++ build is now handled by generating a CMakeLists.txt and building with CMake (Ninja generator) instead of relying on verilator's `--exe --build` flags. If cmake+ninja don't exist, use 'make' as a fallback. Key changes: - Default binary changed from `verilator` to `verilator_bin` - Added `_find_verilator_root()` to locate VERILATOR_ROOT - Removed `--exe`, `--build`, `-CFLAGS`, `-LDFLAGS`, and driver.cpp from the verilator command (these are now handled in the CMake build) - Added `_write_cmake()` to generate a CMakeLists.txt for building - Overrode `compile()` to run verilator_bin then cmake+ninja - Updated `run_command()` for the new executable location - Added unit tests for the new functionality --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: teqdruid <1498080+teqdruid@users.noreply.github.com> Co-authored-by: John Demme <jodemme@microsoft.com> AI-assisted-by: Claude Opus 4.6 (local run)
1 parent e7b7e76 commit ba22599

File tree

3 files changed

+438
-15
lines changed

3 files changed

+438
-15
lines changed

lib/Dialect/ESI/runtime/python/esiaccel/cosim/verilator.py

Lines changed: 160 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
44

55
import os
6+
import shutil
67
from pathlib import Path
78
from typing import List, Optional, Callable, Dict
89

910
from .simulator import CosimCollateralDir, Simulator, SourceFiles
1011

1112

1213
class Verilator(Simulator):
13-
"""Run and compile funcs for Verilator."""
14+
"""Run and compile funcs for Verilator.
15+
16+
Calls ``verilator_bin`` directly (bypassing the Perl wrapper) to generate
17+
C++ from RTL, then builds the simulation executable with CMake + Ninja.
18+
Falls back to ``make`` when cmake/ninja are not available."""
1419

1520
DefaultDriver = CosimCollateralDir / "driver.cpp"
1621

@@ -39,13 +44,64 @@ def __init__(
3944
make_default_logs=make_default_logs,
4045
macro_definitions=macro_definitions,
4146
)
42-
self.verilator = "verilator"
47+
self.verilator_bin = "verilator_bin"
4348
if "VERILATOR_PATH" in os.environ:
44-
self.verilator = os.environ["VERILATOR_PATH"]
49+
vpath = os.environ["VERILATOR_PATH"]
50+
# Backwards compatibility: if the env var points to the Perl wrapper,
51+
# redirect to verilator_bin.
52+
basename = Path(vpath).stem
53+
if basename == "verilator":
54+
self.verilator_bin = str(Path(vpath).parent / "verilator_bin")
55+
else:
56+
self.verilator_bin = vpath
57+
58+
def _find_verilator_root(self) -> Path:
59+
"""Locate VERILATOR_ROOT for runtime includes and sources.
60+
61+
Checks the ``VERILATOR_ROOT`` environment variable first, then attempts
62+
to derive the root from the location of ``verilator_bin``. Supports both
63+
source-tree layouts (``$ROOT/include/verilated.h``) and system package
64+
layouts (``$PREFIX/share/verilator/include/verilated.h``)."""
65+
if "VERILATOR_ROOT" in os.environ:
66+
root = Path(os.environ["VERILATOR_ROOT"])
67+
if root.is_dir():
68+
return root
69+
# verilator_bin is typically in $PREFIX/bin/
70+
verilator_bin_path = shutil.which(self.verilator_bin)
71+
if verilator_bin_path:
72+
prefix = Path(verilator_bin_path).resolve().parent.parent
73+
# Source-tree layout: $VERILATOR_ROOT/bin/verilator_bin
74+
if (prefix / "include" / "verilated.h").exists():
75+
return prefix
76+
# System package layout: $PREFIX/share/verilator/include/verilated.h
77+
pkg_root = prefix / "share" / "verilator"
78+
if (pkg_root / "include" / "verilated.h").exists():
79+
return pkg_root
80+
raise RuntimeError(
81+
"Cannot find VERILATOR_ROOT. Set the VERILATOR_ROOT environment "
82+
"variable or ensure verilator_bin is in PATH.")
83+
84+
@property
85+
def _use_cmake(self) -> bool:
86+
"""True when both cmake and ninja are available on PATH."""
87+
return shutil.which("cmake") is not None and \
88+
shutil.which("ninja") is not None
4589

4690
def compile_commands(self) -> List[List[str]]:
91+
"""Return the commands for the full compile flow.
92+
93+
When cmake and ninja are available the returned list contains three
94+
commands run sequentially:
95+
1. ``verilator_bin`` – generates C++ from RTL.
96+
2. ``cmake`` – configures the C++ build (Ninja generator).
97+
3. ``ninja`` – builds the simulation executable.
98+
99+
Otherwise falls back to two commands:
100+
1. ``verilator_bin --exe`` – generates C++ and a Makefile.
101+
2. ``make`` – builds via the generated Makefile.
102+
"""
47103
cmd: List[str] = [
48-
self.verilator,
104+
self.verilator_bin,
49105
"--cc",
50106
]
51107

@@ -62,30 +118,115 @@ def compile_commands(self) -> List[List[str]]:
62118
"-Wno-TIMESCALEMOD",
63119
"-Wno-fatal",
64120
"-sv",
65-
"--exe",
66-
"--build",
67121
"-j",
68122
"0",
69123
"--output-split",
70124
"--autoflush",
71125
"--assert",
72-
str(Verilator.DefaultDriver),
73-
]
74-
cflags = [
75-
"-DTOP_MODULE=" + self.sources.top,
76126
]
77127
if self.debug:
78128
cmd += [
79129
"--trace-fst", "--trace-params", "--trace-structs",
80130
"--trace-underscore"
81131
]
132+
133+
if self._use_cmake:
134+
cmd += [str(p) for p in self.sources.rtl_sources]
135+
build_dir = str(Path.cwd() / "obj_dir" / "cmake_build")
136+
cmake_cmd = ["cmake", "-G", "Ninja", "-S", build_dir, "-B", build_dir]
137+
ninja_cmd = ["ninja", "-C", build_dir]
138+
return [cmd, cmake_cmd, ninja_cmd]
139+
140+
# -- make fallback --
141+
# Let verilator generate a Makefile with --exe so it includes the
142+
# driver, CFLAGS, and LDFLAGS directly.
143+
cmd += ["--exe", str(Verilator.DefaultDriver)]
144+
cflags = ["-DTOP_MODULE=" + self.sources.top]
145+
if self.debug:
82146
cflags.append("-DTRACE")
83-
if len(cflags) > 0:
84-
cmd += ["-CFLAGS", " ".join(cflags)]
85-
if len(self.sources.dpi_so) > 0:
147+
cmd += ["-CFLAGS", " ".join(cflags)]
148+
if self.sources.dpi_so:
86149
cmd += ["-LDFLAGS", " ".join(["-l" + so for so in self.sources.dpi_so])]
87150
cmd += [str(p) for p in self.sources.rtl_sources]
88-
return [cmd]
151+
top = self.sources.top
152+
make_cmd = ["make", "-C", "obj_dir", "-f", f"V{top}.mk", "-j"]
153+
return [cmd, make_cmd]
154+
155+
def _write_cmake(self, obj_dir: Path) -> Path:
156+
"""Write a CMakeLists.txt for building the verilated simulation.
157+
158+
Returns the path to the CMake build directory."""
159+
verilator_root = self._find_verilator_root()
160+
include_dir = verilator_root / "include"
161+
exe_name = "V" + self.sources.top
162+
163+
runtime_sources = [
164+
include_dir / "verilated.cpp",
165+
include_dir / "verilated_threads.cpp",
166+
]
167+
if self.sources.dpi_so:
168+
runtime_sources.append(include_dir / "verilated_dpi.cpp")
169+
if self.debug:
170+
runtime_sources.append(include_dir / "verilated_fst_c.cpp")
171+
172+
rt_src = "\n ".join(str(s) for s in runtime_sources)
173+
driver = str(Verilator.DefaultDriver)
174+
inc = str(include_dir)
175+
vltstd = str(include_dir / "vltstd")
176+
177+
defs = [f"TOP_MODULE={self.sources.top}"]
178+
if self.debug:
179+
defs.append("TRACE")
180+
defs_str = "\n ".join(defs)
181+
182+
# Link DPI shared objects by full path.
183+
dpi_link = ""
184+
if self.sources.dpi_so:
185+
dpi_paths = self.sources.dpi_so_paths()
186+
dpi_link = "\n ".join(str(p) for p in dpi_paths)
187+
188+
content = f"""\
189+
cmake_minimum_required(VERSION 3.20)
190+
project({exe_name} CXX)
191+
192+
file(GLOB GENERATED_SOURCES "${{CMAKE_CURRENT_SOURCE_DIR}}/../*.cpp")
193+
194+
add_executable({exe_name}
195+
${{GENERATED_SOURCES}}
196+
{rt_src}
197+
{driver}
198+
)
199+
200+
target_include_directories({exe_name} PRIVATE
201+
{inc}
202+
{vltstd}
203+
${{CMAKE_CURRENT_SOURCE_DIR}}/..
204+
)
205+
206+
target_compile_definitions({exe_name} PRIVATE
207+
{defs_str}
208+
)
209+
210+
find_package(Threads REQUIRED)
211+
target_link_libraries({exe_name} PRIVATE
212+
Threads::Threads
213+
{dpi_link}
214+
)
215+
"""
216+
build_dir = obj_dir / "cmake_build"
217+
build_dir.mkdir(parents=True, exist_ok=True)
218+
(build_dir / "CMakeLists.txt").write_text(content)
219+
return build_dir
220+
221+
def compile(self) -> int:
222+
"""Set VERILATOR_ROOT, write the CMakeLists.txt (if using cmake), then
223+
delegate to the base class which runs all commands from
224+
:meth:`compile_commands`."""
225+
verilator_root = self._find_verilator_root()
226+
os.environ["VERILATOR_ROOT"] = str(verilator_root)
227+
if self._use_cmake:
228+
self._write_cmake(Path.cwd() / "obj_dir")
229+
return super().compile()
89230

90231
@property
91232
def waveform_extension(self) -> str:
@@ -95,5 +236,9 @@ def waveform_extension(self) -> str:
95236
def run_command(self, gui: bool):
96237
if gui:
97238
raise RuntimeError("Verilator does not support GUI mode.")
98-
exe = Path.cwd() / "obj_dir" / ("V" + self.sources.top)
239+
exe_name = "V" + self.sources.top
240+
if self._use_cmake:
241+
exe = Path.cwd() / "obj_dir" / "cmake_build" / exe_name
242+
else:
243+
exe = Path.cwd() / "obj_dir" / exe_name
99244
return [str(exe)]

lib/Dialect/ESI/runtime/tests/integration/sw/test_loopback.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ def test_cosim_loopback(conn: AcceleratorConnection) -> None:
116116
run(conn)
117117

118118

119+
def test_cosim_loopback_make_fallback(monkeypatch) -> None:
120+
"""Run the loopback test using the ``make`` fallback (no cmake/ninja)."""
121+
from esiaccel.cosim.verilator import Verilator
122+
monkeypatch.setattr(Verilator, "_use_cmake", property(lambda self: False))
123+
# Delegate to the already-decorated cosim test. The monkeypatch is
124+
# inherited by the forked child process.
125+
test_cosim_loopback()
126+
127+
119128
if __name__ == "__main__":
120129
platform = sys.argv[1]
121130
connstr = sys.argv[2]

0 commit comments

Comments
 (0)