diff options
author | Andrew Waterman <andrew@sifive.com> | 2025-10-13 16:24:22 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-10-13 16:24:22 -0700 |
commit | 26e2c04c913a67ac51bf0a354f21f2d7d5c07c40 (patch) | |
tree | 7db4f4637f71f1777f86702a2228ccf3b9217a03 /src/riscv_opcodes/svg_utils.py | |
parent | 433081c02368eb72e6dea5413e404a8ef231658e (diff) | |
download | riscv-opcodes-master.zip riscv-opcodes-master.tar.gz riscv-opcodes-master.tar.bz2 |
Add pyproject.toml (the modern alternative to requirements.txt), making this a proper Python package that can be installed via pip and potentially uploaded to PyPI.
This also loads the files using `importlib.resources` and installs them into the wheel. This means that when you create a wheel using `uv build` it will still be able to load all the opcodes and CSV files.
To avoid moving those resource files in the source repo, the Python build backend (hatchling) is instructed to move them to the right place when building a wheel, and the `resource_root()` function checks in both places so it always works. This is a little hacky but it works.
CI builds source and binary wheels (not actually binary) that can be uploaded to PyPI. If we do upload them then using this project is as simple as
```
uvx riscv_opcodes -c 'rv*'
```
Co-authored-by: Tim Hutt <timothy.hutt@codasip.com>
Diffstat (limited to 'src/riscv_opcodes/svg_utils.py')
-rw-r--r-- | src/riscv_opcodes/svg_utils.py | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/src/riscv_opcodes/svg_utils.py b/src/riscv_opcodes/svg_utils.py new file mode 100644 index 0000000..4126ad6 --- /dev/null +++ b/src/riscv_opcodes/svg_utils.py @@ -0,0 +1,284 @@ +import logging +import pprint +from typing import Dict, List, NamedTuple + +from .rv_colors import palette +from .shared_utils import InstrDict, instr_dict_2_extensions + +pp = pprint.PrettyPrinter(indent=2) +logging.basicConfig(level=logging.INFO, format="%(levelname)s:: %(message)s") + + +class RectangleDimensions(NamedTuple): + x: float + y: float + w: float + h: float + + +class InstrRectangle(NamedTuple): + dims: RectangleDimensions + extension: str + label: str + + +InstrDimsDict = Dict[str, RectangleDimensions] + + +def encoding_to_rect(encoding: str) -> RectangleDimensions: + """Convert a binary encoding string to rectangle dimensions.""" + + def calculate_size(free_bits: int, tick: float) -> float: + """Calculate size based on number of free bits and tick value.""" + return 2**free_bits * tick + + instr_length = len(encoding) + # starting position + x = 0 + y = 0 + x_tick = 1 / (2 ** (0.5 * instr_length)) + y_tick = 1 / (2 ** (0.5 * instr_length)) + x_free_bits = 0 + y_free_bits = 0 + even = encoding[0::2] + odd = encoding[1::2] + # Process bits from least significant to most significant + for i, bit in enumerate(encoding): + if bit == "1": + offset = 0.5 / (2 ** int(i / 2)) + if i % 2 == 0: + y += offset + else: + x += offset + elif bit == "0": + pass + # position not adjusted on 0 + + x_free_bits = odd.count("-") + y_free_bits = even.count("-") + x_size = calculate_size(x_free_bits, x_tick) + y_size = calculate_size(y_free_bits, y_tick) + + # If we came here, encoding can be visualized with a single rectangle + rectangle = RectangleDimensions(x=x, y=y, w=x_size, h=y_size) + return rectangle + + +FIGSIZE = 128 + + +def plot_image( + instr_dict: InstrDict, + instr_dims_dict: InstrDimsDict, + extension_sizes: Dict[str, float], +) -> None: + """Plot the instruction rectangles using matplotlib.""" + + from matplotlib import patches + from matplotlib import pyplot as plt + + def get_readable_font_color(bg_hex: str) -> str: + """Determine readable font color based on background color.""" + + def hex_to_rgb(hex_color: str) -> tuple[int, int, int]: + """Convert hex color string to RGB tuple.""" + hex_color = hex_color.lstrip("#") + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + + return (r, g, b) + + r, g, b = hex_to_rgb(bg_hex) + luminance = 0.299 * r + 0.587 * g + 0.114 * b + return "#000000" if luminance > 186 else "#FFFFFF" + + def plot_with_matplotlib( + rectangles: list[InstrRectangle], + colors: list[str], + hatches: list[str], + extensions: list[str], + ) -> None: + """Plot rectangles with matplotlib using specified styles.""" + + _, ax = plt.subplots(figsize=(FIGSIZE, FIGSIZE), facecolor="none") # type: ignore + ax.set_facecolor("none") # type: ignore + linewidth = FIGSIZE / 100 + for dims, ext, label in rectangles: + x, y, w, h = dims + ext_idx = extensions.index(ext) + color = colors[ext_idx] + hatch = hatches[ext_idx] + rect = patches.Rectangle( + (x, y), + w, + h, + linewidth=linewidth, + edgecolor="black", + facecolor=color, + hatch=hatch, + alpha=1.0, + ) + ax.add_patch(rect) + + if w >= h: + base_dim = w + rotation = 0 + else: + base_dim = h + rotation = 90 + + # Scale font size based on base dimension and label length + n_chars = len(label) + font_size = ( + base_dim / n_chars * 90 * FIGSIZE + ) # Adjust scaling factor as needed + if font_size > 1: + fontdict = { + "fontsize": font_size, + "color": get_readable_font_color(color), + "family": "DejaVu Sans Mono", + } + ax.text( # type: ignore + x + w / 2, + y + h / 2, + label, + ha="center", + va="center", + fontdict=fontdict, + rotation=rotation, + ) + + plt.axis("off") # type: ignore + plt.tight_layout() # type: ignore + plt.savefig("inst.svg", format="svg") # type: ignore + plt.show() # type: ignore + + extensions: List[str] = sorted( + extension_sizes.keys(), key=lambda k: extension_sizes[k], reverse=True + ) + + rectangles: List[InstrRectangle] = [] + for instr in instr_dict: + dims = instr_dims_dict[instr] + rectangles.append( + InstrRectangle( + dims=dims, + extension=instr_dict[instr]["extension"][0], + label=instr.replace("_", "."), + ) + ) + + # sort rectangles so that small ones are in the foreground + # An overlap occurs e.g. for pseudo ops, and these should be on top of the encoding it reuses + rectangles = sorted(rectangles, key=lambda x: x.dims.w * x.dims.h, reverse=True) + + colors, hatches = generate_styles(extensions) + + plot_with_matplotlib(rectangles, colors, hatches, extensions) + + +def generate_styles(extensions: list[str]) -> tuple[list[str], list[str]]: + """Generate color and hatch styles for extensions.""" + n_colors = len(palette) + colors = [""] * len(extensions) + hatches = [""] * len(extensions) + hatch_options = ["", "/", "\\", "|", "-", "+", "x", ".", "*"] + color_options = list(palette.values()) + + for i in range(len(extensions)): + colors[i] = color_options[i % n_colors] + hatches[i] = hatch_options[int(i / n_colors) % len(hatch_options)] + + return colors, hatches + + +def defragment_encodings( + encodings: list[str], length: int = 32, offset: int = 0 +) -> list[str]: + """Defragment a list of binary encodings by reordering bits.""" + # determine bit position which has the most fixed bits + fixed_encodings = ["0", "1"] + fixed_bits = [0] * length + fixed_encoding_indeces: Dict[str, List[int]] = { + value: [] for value in fixed_encodings + } + for index, encoding in enumerate(encodings): + for position, value in enumerate(encoding): + if position > offset: + if value != "-": + fixed_bits[position] += 1 + + # find bit position with most fixed bits, starting with the LSB to favor the opcode field + max_fixed_bits = max(fixed_bits) + if max_fixed_bits == 0: + # fully defragemented + return encodings + max_fixed_position = len(fixed_bits) - 1 - fixed_bits[::-1].index(max_fixed_bits) + + # move bit position with the most fixed bits to the front + for index, encoding in enumerate(encodings): + encodings[index] = ( + encoding[0:offset] + + encoding[max_fixed_position] + + encoding[offset:max_fixed_position] + + encoding[max_fixed_position + 1 :] + ) + + if encoding[max_fixed_position] in fixed_encodings: + fixed_encoding_indeces[encoding[max_fixed_position]].append(index) + else: + # No more fixed bits in this encoding + pass + + if offset < length: + # continue to defragement starting from the next offset + offset = offset + 1 + + # separate encodings + sep_encodings: Dict[str, List[str]] = {} + for fixed_encoding in fixed_encodings: + sep_encodings[fixed_encoding] = [ + encodings[i] for i in fixed_encoding_indeces[fixed_encoding] + ] + sep_encodings[fixed_encoding] = defragment_encodings( + sep_encodings[fixed_encoding], length=length, offset=offset + ) + + # join encodings + for new_index, orig_index in enumerate( + fixed_encoding_indeces[fixed_encoding] + ): + encodings[orig_index] = sep_encodings[fixed_encoding][new_index] + + return encodings + + +def defragment_encoding_dict(instr_dict: InstrDict) -> InstrDict: + """Apply defragmentation to the encoding dictionary.""" + encodings = [instr["encoding"] for instr in instr_dict.values()] + encodings_defragemented = defragment_encodings(encodings, length=32, offset=0) + for index, instr in enumerate(instr_dict): + instr_dict[instr]["encoding"] = encodings_defragemented[index] + return instr_dict + + +def make_svg(instr_dict: InstrDict) -> None: + """Generate an SVG image from instruction encodings.""" + extensions = instr_dict_2_extensions(instr_dict) + extension_size: Dict[str, float] = {} + + instr_dict = defragment_encoding_dict(instr_dict) + instr_dims_dict: InstrDimsDict = {} + + for ext in extensions: + extension_size[ext] = 0 + + for instr in instr_dict: + dims = encoding_to_rect(instr_dict[instr]["encoding"]) + + extension_size[instr_dict[instr]["extension"][0]] += dims.h * dims.w + + instr_dims_dict[instr] = dims + + plot_image(instr_dict, instr_dims_dict, extension_size) |