commit ab1233318423d5477a2ca2d3207e5e1d55b52967 Author: Lord Baryhobal Date: Fri Nov 24 14:34:44 2023 +0100 initial commit 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