aboutsummaryrefslogtreecommitdiff
path: root/bin/modules/world.py
diff options
context:
space:
mode:
authorPaul Oliver <contact@pauloliver.dev>2024-02-29 02:29:14 +0100
committerPaul Oliver <contact@pauloliver.dev>2024-02-29 02:29:14 +0100
commit2250b4db92bd272dbb1fd717eb791e293c17e37a (patch)
tree0bde7ed8a2ba8b2a04da629b0317568dea2a6b3c /bin/modules/world.py
parentde427d319c699b8bed7ed73289b3698f13ac3acc (diff)
Store python modules on './bin/modules/' subdirectory. [#37]
Diffstat (limited to 'bin/modules/world.py')
-rw-r--r--bin/modules/world.py279
1 files changed, 279 insertions, 0 deletions
diff --git a/bin/modules/world.py b/bin/modules/world.py
new file mode 100644
index 0000000..08f2734
--- /dev/null
+++ b/bin/modules/world.py
@@ -0,0 +1,279 @@
+""" SALIS: Viewer/controller for the SALIS simulator.
+
+File: world.py
+Author: Paul Oliver
+Email: paul.t.oliver.design@gmail.com
+
+This module should be considered an extension of the 'printer' module. It takes
+care of getting a pre-redered image from Salis and post-processing it in order
+to print it into the curses screen. It also keeps track of user controllable
+rendering parameters (position and zoom).
+"""
+
+import curses
+
+from ctypes import c_uint8, cast, POINTER
+
+
+class World:
+ PADDING = 25
+
+ def __init__(self, printer, sim):
+ """ World constructor. We link to the printer and main simulation
+ classes. We also setup the colors for rendering the world.
+ """
+ self.__printer = printer
+ self.__sim = sim
+ self.__set_world_colors()
+ self.__show_ip = True
+ self.pos = 0
+ self.zoom = 1
+
+ def render(self):
+ """ Function for rendering the world. We get a pre-rendered buffer from
+ Salis' memory module (its way faster to pre-render in C) and use that
+ to assemble the world image in Python.
+ """
+ # Window is so narrow that world is not visible.
+ if self.__printer.size[1] <= self.PADDING:
+ return
+
+ # Get pre-rendered image from Salis' memory module.
+ line_width = self.__printer.size[1] - self.PADDING
+ print_area = self.__printer.size[0] * line_width
+ c_buffer = (c_uint8 * print_area)()
+ self.__sim.lib.sal_ren_get_image(
+ self.pos, self.zoom, print_area, cast(c_buffer, POINTER(c_uint8))
+ )
+
+ # Get data elements of selected process, if it's running, and store
+ # them into a convenient dict object.
+ if self.__sim.lib.sal_proc_is_free(self.__printer.selected_proc):
+ sel_data = None
+ else:
+ out_data = self.__printer.selected_proc_data
+ out_elem = self.__printer.proc_elements
+ sel_data = {
+ "ip": out_data[out_elem.index("ip")],
+ "sp": out_data[out_elem.index("sp")],
+ "mb1a": out_data[out_elem.index("mb1a")],
+ "mb1s": out_data[out_elem.index("mb1s")],
+ "mb2a": out_data[out_elem.index("mb2a")],
+ "mb2s": out_data[out_elem.index("mb2s")],
+ }
+
+ # Iterate all cells on printable area and print the post-rendered
+ # cells. Rendered cells contain info about bit flags and instructions
+ # currently written into memory.
+ bidx = 0
+
+ for y in range(self.__printer.size[0]):
+ for x in range(line_width):
+ xpad = x + self.PADDING
+ addr = self.pos + (self.zoom * bidx)
+ symb, attr = self.__render_cell(c_buffer[bidx], addr, sel_data)
+
+ # Curses raises an exception when printing on the edge of the
+ # screen; we can just ignore it.
+ try:
+ self.__printer.screen.addstr(y, xpad, symb, attr)
+ except curses.error:
+ pass
+
+ bidx += 1
+
+ def zoom_out(self):
+ """ Zoom out by a factor of 2 (zoom *= 2).
+ """
+ if self.__is_world_editable():
+ self.zoom = min(self.zoom * 2, self.__get_max_zoom())
+
+ def zoom_in(self):
+ """ Zoom in by a factor of 2 (zoom //= 2).
+ """
+ if self.__is_world_editable():
+ self.zoom = max(self.zoom // 2, 1)
+
+ def zoom_reset(self):
+ """ Reset zoom to a valid value on certain events (i.e. during terminal
+ resizing).
+ """
+ self.zoom = min(self.zoom, self.__get_max_zoom())
+
+ def pan_left(self):
+ """ Pan world to the left (pos -= zoom).
+ """
+ if self.__is_world_editable():
+ self.pos = max(self.pos - self.zoom, 0)
+
+ def pan_right(self):
+ """ Pan world to the right (pos += zoom).
+ """
+ if self.__is_world_editable():
+ max_pos = self.__sim.lib.sal_mem_get_size() - 1
+ self.pos = min(self.pos + self.zoom, max_pos)
+
+ def pan_down(self, fast=False):
+ """ Pan world downward.
+ """
+ if self.__is_world_editable():
+ if fast:
+ self.pos = max(self.pos - self.__get_world_area(), 0)
+ else:
+ self.pos = max(self.pos - self.__get_line_area(), 0)
+
+ def pan_up(self, fast=False):
+ """ Pan world upward.
+ """
+ if self.__is_world_editable():
+ max_pos = self.__sim.lib.sal_mem_get_size() - 1
+
+ if fast:
+ self.pos = min(self.pos + self.__get_world_area(), max_pos)
+ else:
+ self.pos = min(self.pos + self.__get_line_area(), max_pos)
+
+ def pan_reset(self):
+ """ Set world position to zero.
+ """
+ if self.__is_world_editable():
+ self.pos = 0
+
+ def scroll_to(self, pos):
+ """ Move world pos to a specified position.
+ """
+ if self.__is_world_editable():
+ if self.__sim.lib.sal_mem_is_address_valid(pos):
+ self.pos = pos
+ else:
+ raise RuntimeError("Error: scrolling to an invalid address")
+
+ def toggle_ip_view(self):
+ """ Turn on/off IP visualization. Turning off IPs might make it easier
+ to visualize the underlying memory block structure.
+ """
+ if self.__is_world_editable():
+ self.__show_ip = not self.__show_ip
+
+
+ ###############################
+ # Private methods
+ ###############################
+
+ def __set_world_colors(self):
+ """ Define color pairs for rendering the world. Each color has a
+ special meaning, referring to the selected process IP, SP and memory
+ blocks, or to bit flags currently set on rendered cells.
+ """
+ self.pair_free = self.__printer.get_color_pair(
+ curses.COLOR_BLUE
+ )
+ self.pair_alloc = self.__printer.get_color_pair(
+ curses.COLOR_BLACK, curses.COLOR_BLUE
+ )
+ self.pair_mbstart = self.__printer.get_color_pair(
+ curses.COLOR_BLACK, curses.COLOR_CYAN
+ )
+ self.pair_ip = self.__printer.get_color_pair(
+ curses.COLOR_BLACK, curses.COLOR_WHITE
+ )
+ self.pair_sel_mb2 = self.__printer.get_color_pair(
+ curses.COLOR_BLACK, curses.COLOR_GREEN
+ )
+ self.pair_sel_mb1 = self.__printer.get_color_pair(
+ curses.COLOR_BLACK, curses.COLOR_YELLOW
+ )
+ self.pair_sel_sp = self.__printer.get_color_pair(
+ curses.COLOR_BLACK, curses.COLOR_MAGENTA
+ )
+ self.pair_sel_ip = self.__printer.get_color_pair(
+ curses.COLOR_BLACK, curses.COLOR_RED
+ )
+
+ def __render_cell(self, byte, addr, sel_data=None):
+ """ Render a single cell on the WORLD view. All cells are rendered by
+ interpreting the values coming in from the buffer. We overlay special
+ colors for representing the selected organism's state, on top of the
+ more common colors used to represent memory state.
+ """
+ # Paint black all cells that are out of memory bounds.
+ if not self.__sim.lib.sal_mem_is_address_valid(addr):
+ return " ", curses.A_NORMAL
+
+ # Check if cell contains part of the currently selected process.
+ if sel_data:
+ top_addr = addr + self.zoom
+ top_mb1a = sel_data["mb1a"] + sel_data["mb1s"]
+ top_mb2a = sel_data["mb2a"] + sel_data["mb2s"]
+
+ if addr <= sel_data["ip"] < top_addr:
+ pair = self.pair_sel_ip
+ elif addr <= sel_data["sp"] < top_addr:
+ pair = self.pair_sel_sp
+ elif top_addr > sel_data["mb1a"] and top_mb1a > addr:
+ pair = self.pair_sel_mb1
+ elif top_addr > sel_data["mb2a"] and top_mb2a > addr:
+ pair = self.pair_sel_mb2
+
+ # No pair has been selected yet; select pair based on bit-flags.
+ if not "pair" in locals():
+ if self.__show_ip and byte >= 0x80:
+ pair = self.pair_ip
+ elif (byte % 0x80) >= 0x40:
+ pair = self.pair_mbstart
+ elif (byte % 0x40) >= 0x20:
+ pair = self.pair_alloc
+ else:
+ pair = self.pair_free
+
+ # Select symbol to represent instructions currently on cell.
+ inst = byte % 32
+
+ if self.zoom == 1:
+ symb = self.__printer.inst_list[inst][1]
+ elif inst > 16:
+ symb = ":"
+ else:
+ symb = "."
+
+ # Return tuple containing our post-redered cell.
+ return symb, curses.color_pair(pair)
+
+ def __get_max_zoom(self):
+ """ Calculate maximum needed zoom so that the entire world fits on the
+ terminal window.
+ """
+ max_zoom = 1
+ line_size = self.__printer.size[1] - self.PADDING
+ coverage = self.__printer.size[0] * line_size
+
+ # We fix a maximum zoom level; otherwise, program may halt on extreme
+ # zoom levels.
+ while (
+ (coverage * max_zoom) < self.__sim.lib.sal_mem_get_size() and
+ max_zoom < 2 ** 16
+ ):
+ max_zoom *= 2
+
+ return max_zoom
+
+ def __is_world_editable(self):
+ """ For this to return True, printer's current page must be WORLD page.
+ Additionally, the WORLD panel must be visible on the terminal window
+ (i.e. curses.COLS > data_margin).
+ """
+ correct_page = self.__printer.current_page == "WORLD"
+ correct_size = self.__printer.size[1] > self.PADDING
+ return correct_page and correct_size
+
+ def __get_line_area(self):
+ """ Return amount of bytes contained in a printed WORLD line.
+ """
+ line_size = self.__printer.size[1] - self.PADDING
+ line_area = self.zoom * line_size
+ return line_area
+
+ def __get_world_area(self):
+ """ Return amount of bytes contained in the entire WORLD view.
+ """
+ return self.__get_line_area() * self.__printer.size[0]