From c06230ca0863f893cca82a62a414e6b123318aa3 Mon Sep 17 00:00:00 2001
From: umarcor
Date: Tue, 20 Oct 2020 09:23:27 +0200
Subject: [PATCH] vga/test: add co-simulation with tkinter window
---
vga/README.md | 20 ++++++++--
vga/test/hdl/VGA_screen_pkg.vhd | 8 ++--
vga/test/tkinter/caux.c | 33 ++++++++++++++++
vga/test/tkinter/py.ver | 6 +++
vga/test/tkinter/requirements.txt | 2 +
vga/test/tkinter/run.py | 63 +++++++++++++++++++++++++++++++
vga/test/tkinter/run.sh | 42 +++++++++++++++++++++
vga/test/tkinter/utils.py | 45 ++++++++++++++++++++++
8 files changed, 211 insertions(+), 8 deletions(-)
create mode 100644 vga/test/tkinter/caux.c
create mode 100644 vga/test/tkinter/py.ver
create mode 100644 vga/test/tkinter/requirements.txt
create mode 100644 vga/test/tkinter/run.py
create mode 100644 vga/test/tkinter/run.sh
create mode 100644 vga/test/tkinter/utils.py
diff --git a/vga/README.md b/vga/README.md
index 25fba45..94b58c8 100644
--- a/vga/README.md
+++ b/vga/README.md
@@ -66,15 +66,27 @@ Sources in [test/hdl/](test/hdl) provide a *Virtual VGA screen* based on the [VG
-### Imagemagick
+### Imagemagick (animated GIF)
-[test/hdl/imagemagick/](test/hdl/imagemagick) provides a backend for the virtual screen based on [Imagemagick](https://www.imagemagick.org/). `save_screenshot` saves each frame to a binary file in RGB24 format. Then, `convert` from Imagemagick is used for generating a PNG screenshot. In `sim_cleanup`, `convert` is used for merging all the PNGs into an animated GIF. Execute the run script for running the simulation:
+[test/imagemagick/](test/imagemagick) provides a backend for the virtual screen based on [Imagemagick](https://www.imagemagick.org/). `save_screenshot` saves each frame to a binary file in RGB24 format. Then, `convert` from Imagemagick is used for generating a PNG screenshot. In `sim_cleanup`, `convert` is used for merging all the PNGs into an animated GIF. Execute the run script for running the simulation:
```sh
-./test/hdl/imagemagick/run.sh
+./test/imagemagick/run.sh
```
-Images are saved to `test/hdl/imagemagick/out/`.
+Images are saved to `test/imagemagick/out/`.
+
+### Tkinter (desktop window)
+
+[test/tkinter/](test/tkinter) provides a backend for the virtual screen based on [tkinter](https://docs.python.org/3/library/tkinter.html), the built-in Python interface to Tcl/Tk. The Tk GUI toolkit is available on most Unix platforms, as well as on Windows systems. [NumPy](https://numpy.org/)'s [ctypeslib](https://numpy.org/doc/stable/reference/routines.ctypeslib.html) and [Pillow](https://python-pillow.org/)'s [ImageTk](https://pillow.readthedocs.io/en/stable/reference/ImageTk.html) are used for transforming the VHDL buffer to an image and for displaying the frames in a desktop window. After installing the dependencies, execute the run script for running the simulation:
+
+```sh
+./test/tkinter/run.sh
+```
+
+A windows is shown on the desktop and it is updated after each frame is captured by the VHDL VGA monitor.
+
+> NOTE: On MSYS2's MINGW64, `numpy` needs to be installed through `pacman`. Furthermore, installing `Pillow` through `pip` requires the packages listed in [pillow.rtfd.io: Building on Windows using MSYS2/MinGW](https://pillow.readthedocs.io/en/stable/installation.html#building-on-windows-using-msys2-mingw).
## Development
diff --git a/vga/test/hdl/VGA_screen_pkg.vhd b/vga/test/hdl/VGA_screen_pkg.vhd
index 6c53963..974da2a 100644
--- a/vga/test/hdl/VGA_screen_pkg.vhd
+++ b/vga/test/hdl/VGA_screen_pkg.vhd
@@ -50,12 +50,12 @@ package body VGA_screen_pkg is
variable raw24: std_logic_vector(31 downto 0);
begin
raw24 := (
- 7 downto 0 => rgb(0),
+ 7 downto 0 => rgb(2),
15 downto 8 => rgb(1),
- 23 downto 16 => rgb(2),
- others => '0'
+ 23 downto 16 => rgb(0),
+ others => '1'
);
- return to_integer(unsigned(raw24));
+ return to_integer(signed(raw24));
end function;
end VGA_screen_pkg;
diff --git a/vga/test/tkinter/caux.c b/vga/test/tkinter/caux.c
new file mode 100644
index 0000000..b7dbeb5
--- /dev/null
+++ b/vga/test/tkinter/caux.c
@@ -0,0 +1,33 @@
+#include
+#include
+#include
+#include
+
+extern int ghdl_main(int argc, void** argv);
+
+void (*save_screenshot_cb)(int32_t*, uint32_t, uint32_t, int);
+void save_screenshot(int32_t *ptr, uint32_t width, uint32_t height, int id) {
+ save_screenshot_cb(ptr, width, height, id);
+}
+
+void (*sim_cleanup_cb)(void);
+void sim_cleanup(void) {
+ sim_cleanup_cb();
+}
+
+int py_main(
+ int argc,
+ void** argv,
+ void (*fptr_save_screenshot)(int32_t*, uint32_t, uint32_t, int),
+ void (*fptr_sim_cleanup)(void)
+) {
+ printf("fptr_save_screenshot is %p\n", (void*)fptr_save_screenshot);
+ assert(fptr_save_screenshot != NULL);
+ save_screenshot_cb = fptr_save_screenshot;
+
+ printf("fptr_sim_cleanup is %p\n", (void*)fptr_sim_cleanup);
+ assert(fptr_sim_cleanup != NULL);
+ sim_cleanup_cb = fptr_sim_cleanup;
+
+ return ghdl_main(argc, argv);
+}
diff --git a/vga/test/tkinter/py.ver b/vga/test/tkinter/py.ver
new file mode 100644
index 0000000..8579024
--- /dev/null
+++ b/vga/test/tkinter/py.ver
@@ -0,0 +1,6 @@
+VHPIDIRECT {
+ global:
+py_main;
+ local:
+ *;
+};
diff --git a/vga/test/tkinter/requirements.txt b/vga/test/tkinter/requirements.txt
new file mode 100644
index 0000000..ec18478
--- /dev/null
+++ b/vga/test/tkinter/requirements.txt
@@ -0,0 +1,2 @@
+numpy
+Pillow
\ No newline at end of file
diff --git a/vga/test/tkinter/run.py b/vga/test/tkinter/run.py
new file mode 100644
index 0000000..8d3607d
--- /dev/null
+++ b/vga/test/tkinter/run.py
@@ -0,0 +1,63 @@
+from pathlib import Path
+import ctypes
+
+from io import BytesIO
+
+import numpy as np
+from PIL import Image, ImageTk
+from tkinter import Tk, Label
+
+from utils import dlopen, dlclose, enc_args, FUNCTYPE
+
+
+root = Tk()
+root.title("[DBHI/vboard] VGA screen")
+panel = Label(root, bd=0)
+panel.pack()
+
+
+def save_screenshot(img, width, height, id):
+ print(" Python save_screenshot:", img, width, height, id)
+ image = Image.fromarray(np.ctypeslib.as_array(img, shape=(height, width)))
+ image.mode = 'RGBA'
+
+ # TODO: passing 'image' to panel.image should be possible without writting to an intermediate buffer
+ buf = BytesIO()
+ image.save(buf, format="PNG")
+
+ pimg = ImageTk.PhotoImage(Image.open(buf))
+ panel.configure(image=pimg)
+ panel.image = pimg
+ root.update()
+
+
+def sim_cleanup():
+ print(" Python sim_cleanup!")
+
+
+def run_sim():
+ C_SAVE_SCREENSHOT = FUNCTYPE(
+ None,
+ ctypes.POINTER(ctypes.c_int),
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ctypes.c_int
+ )(save_screenshot)
+
+ C_SIM_CLEANUP = FUNCTYPE(
+ None
+ )(sim_cleanup)
+
+ # TODO pass argv to GHDL
+ C_ARGS = enc_args([str(Path(__file__))])
+
+ CAUXDLL = dlopen("./caux.so")
+
+ print("> pymain")
+ print(CAUXDLL.py_main(len(C_ARGS), C_ARGS, C_SAVE_SCREENSHOT, C_SIM_CLEANUP))
+
+ dlclose(CAUXDLL)
+
+
+root.after(0, run_sim)
+root.mainloop()
diff --git a/vga/test/tkinter/run.sh b/vga/test/tkinter/run.sh
new file mode 100644
index 0000000..7c8363c
--- /dev/null
+++ b/vga/test/tkinter/run.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env sh
+
+set -e
+
+cd $(dirname "$0")
+
+PY="python3"
+if ! command -v "$PY"; then
+ PY="python"
+fi
+
+echo "> Analyze ../src/*.vhd and ./hdl/*.vhd"
+ghdl -a --std=08 -frelaxed ../../src/VGA_config_pkg.vhd
+ghdl -a --std=08 -frelaxed ../../src/VGA_sync_gen_idx.vhd
+ghdl -a --std=08 -frelaxed ../../src/VGA_sync_gen.vhd
+ghdl -a --std=08 -frelaxed ../../src/VGA_sync_gen_cfg.vhd
+ghdl -a --std=08 -frelaxed ../../src/Design_Top.vhd
+ghdl -a --std=08 -frelaxed ../../src/demo.vhd
+
+ghdl -a --std=08 -frelaxed ../hdl/VGA_screen_pkg.vhd
+ghdl -a --std=08 -frelaxed ../hdl/VGA_screen.vhd
+ghdl -a --std=08 -frelaxed ../hdl/VGA_tb.vhd
+
+echo "> Build caux.so"
+ghdl -e \
+ --std=08 \
+ -frelaxed \
+ -Wl,-fPIC \
+ -Wl,caux.c \
+ -Wl,-shared \
+ -Wl,-Wl,--version-script=./py.ver \
+ -Wl,-Wl,-u,ghdl_main \
+ -o caux.so \
+ tb_vga
+
+rm *.o *.cf
+
+#echo "> Execute tb (save wave.ghw)"
+#./tb --wave=wave.ghw
+
+echo "> Execute run.py"
+$PY run.py --wave=wave.ghw
diff --git a/vga/test/tkinter/utils.py b/vga/test/tkinter/utils.py
new file mode 100644
index 0000000..97ae3dc
--- /dev/null
+++ b/vga/test/tkinter/utils.py
@@ -0,0 +1,45 @@
+import sys
+from sys import platform
+from pathlib import Path
+import ctypes
+import _ctypes # type: ignore
+
+
+FUNCTYPE = ctypes.WINFUNCTYPE if platform == 'win32' else ctypes.CFUNCTYPE
+
+def dlopen(path):
+ """
+ Open/load a PIE binary or a shared library.
+ """
+ if not Path(path).is_file():
+ print("Executable binary not found: " + path)
+ sys.exit(1)
+ try:
+ return ctypes.CDLL(path)
+ except OSError:
+ print(
+ "Loading executables dynamically seems not to be supported on this platform"
+ )
+ sys.exit(1)
+
+
+def dlclose(obj):
+ """
+ Close/unload a PIE binary or a shared library.
+ :param obj: object returned by ctypes.CDLL when the resource was loaded
+ """
+ if platform == "win32":
+ _ctypes.FreeLibrary(obj._handle) # pylint:disable=protected-access,no-member
+ else:
+ _ctypes.dlclose(obj._handle) # pylint:disable=protected-access,no-member
+
+
+def enc_args(args):
+ """
+ Convert args to a suitable format for a foreign C function.
+ :param args: list of strings
+ """
+ C_ARGS = (ctypes.POINTER(ctypes.c_char) * len(args))()
+ for idx, arg in enumerate(args):
+ C_ARGS[idx] = ctypes.create_string_buffer(arg.encode("utf-8"))
+ return C_ARGS