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