From ab1233318423d5477a2ca2d3207e5e1d55b52967 Mon Sep 17 00:00:00 2001 From: Lord Baryhobal Date: Fri, 24 Nov 2023 14:34:44 +0100 Subject: [PATCH] initial commit --- .vscode/launch.json | 23 ++++ __init__.py | 0 config.json | 21 ++++ config.py | 10 ++ example1.yaml | 73 ++++++++++++ example2.yaml | 64 +++++++++++ main.py | 6 + range.py | 50 +++++++++ renderer.py | 263 ++++++++++++++++++++++++++++++++++++++++++++ schema.py | 26 +++++ structure.py | 35 ++++++ vec.py | 27 +++++ 12 files changed, 598 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 __init__.py create mode 100644 config.json create mode 100644 config.py create mode 100644 example1.yaml create mode 100644 example2.yaml create mode 100644 main.py create mode 100644 range.py create mode 100644 renderer.py create mode 100644 schema.py create mode 100644 structure.py create mode 100644 vec.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5152def --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": true + }, + { + "name": "main.py", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/main.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config.json b/config.json new file mode 100644 index 0000000..7b7f8e1 --- /dev/null +++ b/config.json @@ -0,0 +1,21 @@ +{ + "defaultFontFamily": "Ubuntu Mono", + "defaultFontSize": 16, + "italicFontFamily": "Ubuntu Mono", + "italicFontSize": 14, + "backgroundColor": [255, 255, 255], + "textColor": [0, 0, 0], + "linkColor": [0, 0, 0], + "bitIColor": [0, 0, 0], + "borderColor": [0, 0, 0], + "bitWidth": 30, + "bitHeight": 30, + "descriptionMargin": 10, + "dashLength": 6, + "dashSpace": 4, + "arrowSize": 10, + "margins": [20, 20, 20, 20], + "arrowMargin": 4, + "valuesGap": 5, + "arrowLabelDistance": 5 +} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..7b0b010 --- /dev/null +++ b/config.py @@ -0,0 +1,10 @@ +import json +from typing import Any + +class Config: + def __init__(self) -> None: + with open("config.json", "r") as f: + self.params = json.load(f) + + def get(self, param: str) -> Any: + return self.params[param] \ No newline at end of file diff --git a/example1.yaml b/example1.yaml new file mode 100644 index 0000000..2ca7964 --- /dev/null +++ b/example1.yaml @@ -0,0 +1,73 @@ +structures: + main: + bits: 32 + ranges: + 31-28: + name: cond + 27: + name: 0 + 26: + name: 1 + 25: + name: I + 24: + name: P + description: pre / post indexing bit + values: + 0: post, add offset after transfer + 1: pre, add offset before transfer + 23: + name: U + description: up / down bit + values: + 0: down, subtract offset from base + 1: up, addition offset to base + 22: + name: B + description: byte / word bit + values: + 0: transfer word quantity + 1: transfer byte quantity + 21: + name: W + description: write-back bit + values: + 0: no write-back + 1: write address into base + 20: + name: L + description: load / store bit + values: + 0: store to memory + 1: load from memory + 19-16: + name: Rn + description: base register + 15-12: + name: Rd + description: source / destination register + 11-0: + name: offset + depends-on: 25 + values: + 0: + description: offset is an immediate value + structure: immediateOffset + 1: + description: offset is a register + structure: registerOffset + immediateOffset: + bits: 12 + ranges: + 11-0: + name: 12-bit immediate offset + description: unsigned number + registerOffset: + bits: 12 + ranges: + 11-4: + name: shift + description: shift applied to Rm + 3-0: + name: Rm + description: offset register diff --git a/example2.yaml b/example2.yaml new file mode 100644 index 0000000..31dc0ef --- /dev/null +++ b/example2.yaml @@ -0,0 +1,64 @@ +structures: + main: + bits: 32 + ranges: + 31-8: + name: '-' + 7-6: + name: code + 5-4: + name: params + description: parameter + values: + 00: unsigned integer + 01: signed integer + 10: unsigned float + 11: signed float + 3-0: + name: values + depends-on: 7-6 + values: + 00: + description: increment register + structure: valuesIncrReg + 01: + description: add registers + structure: valuesAddReg + 10: + description: add immediate value and register + structure: valuesAddImmReg + 11: + description: right shift register by value + structure: valuesShiftReg + valuesIncrReg: + bits: 4 + ranges: + 3-0: + name: register address + valuesAddReg: + bits: 4 + ranges: + 3-2: + name: r1 + description: first register address + 1-0: + name: r2 + description: second register address + valuesAddImmReg: + bits: 4 + ranges: + 3-2: + name: val + description: immediate value + 1-0: + name: r1 + description: register address + valuesShiftReg: + bits: 4 + ranges: + 3-2: + name: shift + description: shift amount + 1-0: + name: r1 + description: register address diff --git a/main.py b/main.py new file mode 100644 index 0000000..fd826eb --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +from schema import InstructionSetSchema + +if __name__ == "__main__": + schema = InstructionSetSchema("example1.yaml") + schema.save("example1_v2.jpg") + input() \ No newline at end of file diff --git a/range.py b/range.py new file mode 100644 index 0000000..fe4b6c4 --- /dev/null +++ b/range.py @@ -0,0 +1,50 @@ +from __future__ import annotations +from typing import Union + +class Range: + def __init__(self, + start: int, + end: int, + name: str, + description: str = "", + values: dict[str, Union[str, dict]] = None, + dependsOn: str = None) -> None: + + self.start = start + self.end = end + self.name = name + self.description = description + self.values = values + self.dependsOn = dependsOn + + @property + def bits(self) -> int: + return self.end - self.start + 1 + + def load(start: int, end: int, data: dict): + values = None + bits = end - start + 1 + + if "values" in data: + values = {} + for val, desc in data["values"].items(): + val = str(val).zfill(bits) + values[val] = desc + + dependsOn = data.get("depends-on", None) + if dependsOn is not None: + dependsOn = Range.parseSpan(str(dependsOn)) + + return Range(start, + end, + str(data["name"]), + data.get("description", ""), + values, + dependsOn) + + def parseSpan(span: str) -> tuple[int, int]: + startEnd = span.split("-") + if len(startEnd) == 1: startEnd.append(startEnd[0]) + start = int(startEnd[1]) + end = int(startEnd[0]) + return (start, end) \ No newline at end of file diff --git a/renderer.py b/renderer.py new file mode 100644 index 0000000..ec94d5a --- /dev/null +++ b/renderer.py @@ -0,0 +1,263 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from config import Config + from range import Range + from schema import InstructionSetSchema + +import pygame + +from structure import Structure +from vec import Vec + +class Renderer: + WIDTH = 1200 + HEIGHT = 800 + + def __init__(self, config: Config) -> None: + self.config = config + pygame.init() + self.win = pygame.display.set_mode([Renderer.WIDTH, Renderer.HEIGHT]) + self.font = pygame.font.SysFont(self.config.get("defaultFontFamily"), self.config.get("defaultFontSize")) + self.italicFont = pygame.font.SysFont(self.config.get("italicFontFamily"), self.config.get("italicFontSize"), italic=True) + + self.margins = self.config.get("margins") + + def render(self, schema: InstructionSetSchema) -> None: + + self.win.fill(self.config.get("backgroundColor")) + + self.drawStructure(schema.structures["main"], schema.structures, self.margins[3], self.margins[0]) + + pygame.display.flip() + + def save(self, path: str) -> None: + pygame.image.save(self.win, path) + + def drawStructure(self, + struct: Structure, + structures: dict[str, + Structure], + ox: float = 0, + oy: float = 0) -> float: + + bgCol = self.config.get("backgroundColor") + txtCol = self.config.get("textColor") + borderCol = self.config.get("borderColor") + bitW = self.config.get("bitWidth") + bitH = self.config.get("bitHeight") + + bitsX, bitsY = ox, oy + bitH + bitsWidth = struct.bits * bitW + startBit = struct.start + + # Draw rectangle around structure + pygame.draw.rect(self.win, borderCol, [bitsX, bitsY, bitsWidth, bitH], 2) + + for i in range(struct.bits): + bitX = ox + i * bitW + + bitITxt = self.font.render(str(struct.bits - i - 1 + startBit), True, txtCol) + self.win.blit(bitITxt, [ + bitX + (bitW - bitITxt.get_width())/2, + oy + (bitH - bitITxt.get_height())/2 + ]) + + # Draw separator + if i != 0: + pygame.draw.line(self.win, borderCol, [bitX, bitsY], [bitX, bitsY + bitH]) + + ranges = struct.getSortedRanges() + descX = ox + max(0, (struct.bits-12) * bitW) + descY = bitsY + bitH * 2 + + # Names + simple descriptions + for range_ in ranges: + rStartI = struct.bits - range_.end + startBit - 1 + + rStartX = bitsX + rStartI * bitW + rWidth = range_.bits * bitW + + nameTxt = self.font.render(range_.name, True, txtCol) + nameX = rStartX + (rWidth - nameTxt.get_width())/2 + nameY = bitsY + (bitH - nameTxt.get_height())/2 + pygame.draw.rect(self.win, bgCol, [rStartX + bitW/2, nameY, rWidth - bitW, nameTxt.get_height()], 0) + self.win.blit(nameTxt, [nameX, nameY]) + + if range_.description: + descX, descY = self.drawDescription(range_, rStartX, bitsY, rWidth, descX, descY) + + # Dependencies + for range_ in ranges: + if range_.values is not None and range_.dependsOn is not None: + descX, descY = self.drawDependency(struct, structures, bitsX, bitsY, range_, descX, descY) + + return descY + + def drawUnderbracket(self, start: float, end: float, bitsY: float) -> None: + bitW = self.config.get("bitWidth") + bitH = self.config.get("bitHeight") + + x0 = start + bitW/2 + x1 = end - bitW/2 + y0 = bitsY + bitH * 1.25 + y1 = bitsY + bitH * 1.5 + + pygame.draw.lines(self.win, self.config.get("linkColor"), False, [ + [x0, y0], + [x0, y1], + [x1, y1], + [x1, y0] + ]) + + def drawLink(self, startX: float, startY: float, endX: float, endY: float) -> None: + bitH = self.config.get("bitHeight") + arrowMargin = self.config.get("arrowMargin") + + pygame.draw.lines(self.win, self.config.get("linkColor"), False, [ + [startX, startY + bitH*1.5], + [startX, endY + bitH/2], + [endX - arrowMargin, endY + bitH/2] + ]) + + def drawDescription(self, + range_: Range, + rStartX: float, + rStartY: float, + rWidth: float, + descX: float, + descY: float) -> tuple[float, float]: + + bitW = self.config.get("bitWidth") + bitH = self.config.get("bitHeight") + + descX = max(descX, rStartX + rWidth/2 + bitW) + + self.drawUnderbracket(rStartX, rStartX + rWidth, rStartY) + + midX = rStartX + rWidth/2 + self.drawLink(midX, rStartY, descX, descY) + + descTxt = self.font.render(range_.description, True, self.config.get("textColor")) + self.win.blit(descTxt, [descX, descY + (bitH - descTxt.get_height())/2]) + + descY += descTxt.get_height() + + if range_.values is not None and range_.dependsOn is None: + descX, descY = self.drawValues(range_.values, descX, descY) + + descY += self.config.get("descriptionMargin") + + return (descX, descY) + + def drawValues(self, values: dict[str, str], descX: float, descY: float) -> tuple[float, float]: + textCol = self.config.get("textColor") + bitW = self.config.get("bitWidth") + gap = self.config.get("valuesGap") + + for val, desc in sorted(values.items(), key=lambda vd: vd[0]): + descY += gap + valTxt = self.italicFont.render(f"{val} = {desc}", True, textCol) + self.win.blit(valTxt, [descX + bitW/2, descY]) + descY += valTxt.get_height() + + return (descX, descY) + + def drawDependency(self, + struct: Structure, + structures: dict[str, + Structure], + bitsX: float, + bitsY: float, + range_: Range, + descX: float, + descY: float) -> tuple[float, float]: + + bitW = self.config.get("bitWidth") + bitH = self.config.get("bitHeight") + arrowMargin = self.config.get("arrowMargin") + + rStartI = struct.bits - range_.end - 1 + rStartX = bitsX + rStartI * bitW + rWidth = range_.bits * bitW + + self.drawUnderbracket(rStartX, rStartX + rWidth, bitsY) + prevY = bitsY + bitH * 1.5 + dependRange = struct.ranges[range_.dependsOn] + dependStartI = struct.bits - dependRange.end - 1 + dependStartX = bitsX + dependStartI * bitW + dependWidth = dependRange.bits * bitW + dependMid = dependStartX + dependWidth/2 + self.drawUnderbracket(dependStartX, dependStartX + dependWidth, bitsY) + + for val, data in sorted(range_.values.items(), key=lambda vd: vd[0]): + self.drawArrow(dependMid, prevY, dependMid, descY - arrowMargin) + + valRanges = {} + for i in range(dependRange.bits): + valRanges[dependRange.end - i] = { + "name": val[i] + } + + valStruct = { + "bits": dependRange.bits, + "start": dependRange.start, + "ranges": valRanges + } + valStruct = Structure.load("", valStruct) + + self.drawStructure(valStruct, structures, dependStartX, descY) + + valueRight = dependStartX + dependWidth + self.drawArrow(valueRight + arrowMargin, + descY + bitH*1.5, + rStartX - arrowMargin, + descY + bitH*1.5, + data["description"]) + + self.drawArrow(rStartX + rWidth - bitW, + prevY, + rStartX + rWidth - bitW, + descY + bitH - arrowMargin) + + prevY = descY + bitH*2 + arrowMargin + descY = self.drawStructure(structures[data["structure"]], structures, rStartX, descY) + + return (descX, descY) + + def drawArrow(self, sx: float, sy: float, ex: float, ey: float, label: str = "") -> None: + dashLen = self.config.get("dashLength") + dashSpace = self.config.get("dashSpace") + arrowSize = self.config.get("arrowSize") + linkCol = self.config.get("linkColor") + textCol = self.config.get("textColor") + arrowLabelDist = self.config.get("arrowLabelDistance") + + start = Vec(sx, sy) + end = Vec(ex, ey) + startEnd = end - start + d = startEnd.norm() + + dashes = int(startEnd.mag() / (dashLen + dashSpace)) + + for i in range(dashes): + a = start + d * i * (dashLen + dashSpace) + b = a + d*dashLen + pygame.draw.line(self.win, linkCol, [a.x, a.y], [b.x, b.y]) + + n = Vec(d.y, -d.x) + + width = arrowSize / 1.5 + p1 = end - d * arrowSize + n * width + p2 = end - d * arrowSize - n * width + pygame.draw.polygon(self.win, linkCol, [ + [end.x, end.y], + [p1.x, p1.y], + [p2.x, p2.y]], 0) + + if label: + txt = self.italicFont.render(label, True, textCol) + self.win.blit(txt, [ + (start.x + end.x - txt.get_width())/2, + (start.y + end.y)/2 + arrowLabelDist + ]) \ No newline at end of file diff --git a/schema.py b/schema.py new file mode 100644 index 0000000..916c254 --- /dev/null +++ b/schema.py @@ -0,0 +1,26 @@ +import yaml + +from config import Config +from renderer import Renderer +from structure import Structure + +class InstructionSetSchema: + def __init__(self, path: str) -> None: + self.config = Config() + self.path = path + self.load() + + def load(self) -> None: + with open(self.path, "r") as f: + schema = yaml.safe_load(f) + + self.structures = {} + + for id_, data in schema["structures"].items(): + id_ = str(id_) + self.structures[id_] = Structure.load(id_, data) + + def save(self, path: str) -> None: + renderer = Renderer(self.config) + renderer.render(self) + renderer.save(path) \ No newline at end of file diff --git a/structure.py b/structure.py new file mode 100644 index 0000000..6cd1775 --- /dev/null +++ b/structure.py @@ -0,0 +1,35 @@ +from __future__ import annotations +from range import Range + +class Structure: + def __init__(self, + name: str, + bits: int, + ranges: dict[str, Range], + start: int = 0) -> None: + + self.name = name + self.bits = bits + self.ranges = ranges + self.start = start + + def load(id_: str, data: dict) -> Structure: + ranges = {} + for rSpan, rData in data["ranges"].items(): + rStart, rEnd = Range.parseSpan(str(rSpan)) + ranges[(rStart, rEnd)] = Range.load(rStart, rEnd, rData) + + for range_ in ranges.values(): + if range_.values is not None and range_.dependsOn is not None: + bits = ranges[range_.dependsOn].bits + values = {} + for v, d in range_.values.items(): + v = str(int(v)).zfill(bits) + values[v] = d + range_.values = values + + return Structure(id_, data["bits"], ranges, data.get("start", 0)) + + def getSortedRanges(self) -> list[Range]: + ranges = self.ranges.values() + return list(sorted(ranges, key=lambda r: r.end)) \ No newline at end of file diff --git a/vec.py b/vec.py new file mode 100644 index 0000000..8b69f3a --- /dev/null +++ b/vec.py @@ -0,0 +1,27 @@ +from __future__ import annotations +from math import sqrt + +class Vec: + def __init__(self, x: float = 0, y: float = 0) -> None: + self.x = x + self.y = y + + def __add__(self, v: Vec) -> Vec: + return Vec(self.x+v.x, self.y+v.y) + + def __sub__(self, v: Vec) -> Vec: + return Vec(self.x-v.x, self.y-v.y) + + def __mul__(self, f: float) -> Vec: + return Vec(self.x*f, self.y*f) + + def __truediv__(self, f: float) -> Vec: + return Vec(self.x/f, self.y/f) + + def mag(self) -> float: + return sqrt(self.x**2 + self.y**2) + + def norm(self) -> Vec: + mag = self.mag() + if mag == 0: return Vec() + return self/mag \ No newline at end of file