refactor: improve UI rendering, show access heatmap

This commit is contained in:
shokre 2021-10-17 22:54:37 +02:00
parent 63c03e55fa
commit 0d3c3c28a0
9 changed files with 216 additions and 90 deletions

View file

@ -1,7 +1,7 @@
Orao Emulator Orao Emulator
============ ============
![Screenshot](assets/screenshot.png?raw=true) ![Screenshot](assets/screenshot-20211017-225446.png?raw=true)
[Orao](https://en.wikipedia.org/wiki/Orao_%28computer%29) is a Croatian 8-bit [Orao](https://en.wikipedia.org/wiki/Orao_%28computer%29) is a Croatian 8-bit
computer used primarily in elementary schools, as part of a computer literacy computer used primarily in elementary schools, as part of a computer literacy

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

79
orao.py
View file

@ -1,7 +1,7 @@
#!/usr/bin/python2 #!/usr/bin/python2
# -*- coding: utf8 -*- # -*- coding: utf8 -*-
import pygame, numpy, sys, datetime, wave, time import pygame, numpy, sys, datetime
from orao.cpu import CPU from orao.cpu import CPU
from orao.keyboard import listener as orao_kbd_listener from orao.keyboard import listener as orao_kbd_listener
from orao.video import mem_listener as video_mem_listener, terminal from orao.video import mem_listener as video_mem_listener, terminal
@ -10,7 +10,7 @@ from orao.chargen import chargen_init, chargen_draw_str
# views # views
from orao.views.cpu_state import CPUState from orao.views.cpu_state import CPUState
from orao.views.heatmap import MemHeatmap from orao.views.micro_mem_view import MicroMemView
MEM_LOAD_PRG = None MEM_LOAD_PRG = None
@ -36,51 +36,82 @@ chargen_init(cpu.memory[0xE000:])
# views # views
view_cpu_state = CPUState() view_cpu_state = CPUState()
view_heatmap = MemHeatmap()
# ram zero page & stack
view_zp = MicroMemView(start_addr=0x0000, size=0x0200, caption='ZP & stack', disp_width=64)
view_zp.listen(cpu)
# user ram view
view_ram = MicroMemView(start_addr=0x0200, size=0x5E00, caption='RAM', disp_width=128)
view_ram.listen(cpu)
# rom access view
view_rom = MicroMemView(start_addr=0xC000, size=0x4000, caption='ROM', disp_width=256)
view_rom.listen(cpu)
view_screen = MicroMemView(start_addr=0x6000, size=0x2000, disp_width=32)
view_screen.listen(cpu)
# status lines # status lines
status_line = pygame.Surface((64 * 8, 3*8), depth=24) status_line = pygame.Surface((64 * 8, 4*8), depth=24)
status_line.fill((0, 0, 0)) status_line.fill((0, 0, 0))
chargen_draw_str(status_line, 0, 0, 'Orao Emulator v0.1') chargen_draw_str(status_line, 0, 0, 'Orao Emulator v0.1')
# setup screen # setup screen
screen = pygame.display.set_mode(( screen = pygame.display.set_mode((
terminal.get_width() * 2 + 1 + int(max(view_heatmap.width, view_cpu_state.width*1.8)), terminal.get_width() * 2 + 2 + int(max(view_rom.width, view_cpu_state.width * 2)),
terminal.get_height() * 2 + 3*8 + 2 terminal.get_height() * 2 + 3*8 + 2 + 30
)) ))
pygame.display.set_caption('Orao Emulator v0.1') pygame.display.set_caption('Orao Emulator v0.1')
lc = (0xff, 0xcc, 0x00)
chargen_draw_str(status_line, 0, 16, 'F12:', color=lc)
chargen_draw_str(status_line, 24+8, 16, ' SCREENSHOT')
if MEM_LOAD_PRG is not None: if MEM_LOAD_PRG is not None:
chargen_draw_str(status_line, 0, 16, 'F8:', color=(0, 0, 0), bg=(0, 255, 0)) chargen_draw_str(status_line, 0, 24, 'F8:', color=lc)
chargen_draw_str(status_line, 24, 16, ' %s' % MEM_LOAD_PRG) chargen_draw_str(status_line, 24, 24, ' %s' % MEM_LOAD_PRG)
def render_frame(): def render_frame(frame_time_ms):
view_cpu_state.render(cpu) view_cpu_state.render(cpu, frame_time_ms)
view_heatmap.render(cpu) view_zp.render(cpu, frame_time_ms)
view_ram.render(cpu, frame_time_ms)
view_rom.render(cpu, frame_time_ms)
view_screen.render(cpu, frame_time_ms)
# blit # blit
screen.fill((0, 0, 0)) screen.fill((0, 0, 0))
screen.blit(pygame.transform.smoothscale(terminal, (512, 512)), [0, 0]) screen.blit(pygame.transform.scale(terminal, (512, 512)), [0, 0])
chargen_draw_str(status_line, 0, 8, 'Speed: {0:.2f} MHz'.format(ratio)) chargen_draw_str(status_line, 0, 8, 'Speed: {0:.2f} MHz'.format(ratio))
screen.blit(status_line, [0, 512+1]) screen.blit(status_line, [0, 512+1])
lsx = 512 + 1 x = 512 + 1
lsy = 0 y = 0
_, lsy = view_cpu_state.blit(screen, [lsx, lsy], scale=1.8) cx, y = view_cpu_state.blit(screen, [x, y], scale=2)
lsy += 1 y += 5
view_heatmap.blit(screen, [lsx, lsy]) x2, y2 = view_zp.blit(screen, [x, y], scale=4)
y2 += 5
_, y2 = view_ram.blit(screen, [x, y2], scale=2)
y2 += 5
_, _ = view_rom.blit(screen, [x, y2], scale=1)
screen.blit(pygame.transform.scale(view_screen.read_map.surf, (512,512)), [0,0])
screen.blit(pygame.transform.scale(view_screen.write_map.surf, (512,512)), [0,0])
# finish rendering # finish rendering
pygame.display.flip() pygame.display.flip()
clock = pygame.time.Clock()
while running: while running:
before, previous_loop_cycles = datetime.datetime.now(), cpu.cycles before, previous_loop_cycles = datetime.datetime.now(), cpu.cycles
time_elapsed = lambda: (datetime.datetime.now()-before).microseconds + 1
for i in range(5000): for i in range(5000):
cpu.step() cpu.step()
time_elapsed = (datetime.datetime.now()-before).microseconds + 1
clock.tick()
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
running = False running = False
@ -117,21 +148,25 @@ while running:
# HACK: reset stack pointer # HACK: reset stack pointer
cpu.sp = 241 cpu.sp = 241
if pkeys[pygame.K_F12]:
now = datetime.datetime.now() # current date and time
pygame.image.save(screen, "assets/screenshot-%s.png" % now.strftime("%Y%m%d-%H%M%S"))
if event.type == pygame.USEREVENT + 1: if event.type == pygame.USEREVENT + 1:
render_frame() render_frame(clock.get_time())
cpu.tape_out = None if cpu.cycles - cpu.last_sound_cycles > 20000 else cpu.tape_out cpu.tape_out = None if cpu.cycles - cpu.last_sound_cycles > 20000 else cpu.tape_out
if len(cpu.sndbuf) > 4096 or cpu.sndbuf and cpu.cycles - cpu.last_sound_cycles > 20000: if len(cpu.sndbuf) > 4096 or cpu.sndbuf and cpu.cycles - cpu.last_sound_cycles > 20000:
while cpu.channel.get_queue(): while cpu.channel.get_queue():
if time_elapsed() > 10000: break if time_elapsed > 10000: break
cpu.channel.queue(pygame.sndarray.make_sound(numpy.uint8(cpu.sndbuf))) cpu.channel.queue(pygame.sndarray.make_sound(numpy.uint8(cpu.sndbuf)))
cpu.sndbuf = [] cpu.sndbuf = []
overshoot = cpu.cycles - previous_loop_cycles - time_elapsed() overshoot = cpu.cycles - previous_loop_cycles - time_elapsed
pygame.time.wait((overshoot > 0) * overshoot // 1000) # Pričekaj da budemo cycle exact pygame.time.wait((overshoot > 0) * overshoot // 1000) # Pričekaj da budemo cycle exact
ratio = 1.0 * (cpu.cycles - previous_loop_cycles) / time_elapsed() ratio = 1.0 * (cpu.cycles - previous_loop_cycles) / time_elapsed
pygame.quit() pygame.quit()

53
orao/views/access_map.py Normal file
View file

@ -0,0 +1,53 @@
import pygame
from .view import View
# mark accessed locations
class AccessMap(View):
decay_locations = []
def __init__(self, size=0x5F00, start_addr=0, disp_width=256, color=(255, 0, 0)):
self.start_addr = start_addr
self.size = size
self.disp_width = disp_width
dims = (disp_width, size / disp_width)
self.init_surface(pygame.Surface(dims, pygame.SRCALPHA, depth=32))
r,g,b = color
self.surf.fill((r, g, b, 0))
self.writes = []
def render(self, cpu, frame_time_ms):
# decay alfa
decay = int(frame_time_ms*256/500)
new_decay_locations = []
alpha = pygame.surfarray.pixels_alpha(self.surf)
for w in self.decay_locations:
v = alpha[w]
if v > 0:
nv = max(v-decay,0)
alpha[w] = nv
if nv > 0:
new_decay_locations.append(w)
# mark new writes
for w in self.writes:
alpha[w] = 192
new_decay_locations.append(w)
alpha = None
self.writes = []
self.decay_locations = new_decay_locations
def mem_listener(self, addr, val, cpu):
if addr < self.start_addr:
return
addr -= self.start_addr
if addr >= self.size:
return
y, x = divmod(addr, self.disp_width)
self.writes.append((x, y))

View file

@ -1,35 +1,26 @@
import pygame import pygame
from .view import View
from ..chargen import chargen_draw_str from ..chargen import chargen_draw_str
class CPUState:
surf = None class CPUState(View):
def __init__(self): def __init__(self):
self.surf = pygame.Surface((8 * 8, 6 * 8), depth=24) self.init_surface(pygame.Surface((2 * 8 * 8, 3 * 8), depth=24))
self.width = self.surf.get_width() #self.set_smooth_scale()
self.height = self.surf.get_height()
self.surf.fill((0, 0, 0))
def scale(self, f): def draw_text(self, x, y, text, color=(0, 255, 0)):
return pygame.transform.smoothscale(self.surf, (int(self.width * f), int(self.height * f))) chargen_draw_str(self.surf, x, y, text, color=color)
def render(self, cpu): def render(self, cpu, frame_time_ms):
lc = (0xff, 0xcc, 0x00) lc = (0xff, 0xcc, 0x00)
chargen_draw_str(self.surf, 0, 0*8, 'A X Y', color=lc) self.draw_text(0, 0 * 8, 'A X Y', color=lc)
chargen_draw_str(self.surf, 0, 1*8, '%02X %02X %02X' % (cpu.a, cpu.x, cpu.y)) self.draw_text(0, 1 * 8, '%02X %02X %02X' % (cpu.a, cpu.x, cpu.y))
chargen_draw_str(self.surf, 0, 2*8, 'PC:', color=lc) self.draw_text(0, 2 * 8, 'PC:', color=lc)
chargen_draw_str(self.surf, 0, 3*8, 'SP:', color=lc) self.draw_text(8 * 8, 2 * 8, 'SP:', color=lc)
chargen_draw_str(self.surf, 4*8, 2*8, '%04X' % cpu.pc) self.draw_text(3 * 8, 2 * 8, '%04X' % cpu.pc)
chargen_draw_str(self.surf, 4*8, 3*8, '%04X' % cpu.sp) self.draw_text(11 * 8, 2 * 8, '%04X' % cpu.sp)
chargen_draw_str(self.surf, 0, 4*8, "NVssDIZC", color=lc) self.draw_text(8 * 8, 0 * 8, "NVssDIZC", color=lc)
chargen_draw_str(self.surf, 0, 5*8, "{0:b}".format(cpu.flags)) self.draw_text(8 * 8, 1 * 8, "{0:b}".format(cpu.flags))
def blit(self, screen, pos, scale=1):
if scale == 1:
screen.blit(self.surf, pos)
return (pos[0], pos[1] + self.height)
else:
screen.blit(self.scale(scale), pos)
return (pos[0], pos[1] + int(self.height*scale))

View file

@ -1,8 +1,6 @@
import pygame import pygame
import numpy import numpy
from ..chargen import chargen_draw_str from .view import View
SURF_SCALE = 2
palette = [] palette = []
# below chars # below chars
@ -12,51 +10,24 @@ palette += [(i * 2, 255, 0) for i in range(128 - 32)]
# other # other
palette += [(i * 2, 0, 255) for i in range(128)] palette += [(i * 2, 0, 255) for i in range(128)]
# displays memory locations as colors of palette based on value
class MemHeatmap: class MemHeatmap(View):
dims = (32, 192)
surf = None surf = None
tick_color = (0xff, 0xcc, 0x00)
label_color = (0xff, 0xcc, 0x00)
tick_size = 2
start_addr = 0 start_addr = 0
size = 0
def __init__(self, size=16, start_addr=0): def __init__(self, size=0x1000, start_addr=0, disp_width=256):
# mem view surface
self.start_addr = start_addr self.start_addr = start_addr
self.dims = (32, size * 8) self.size = size
self.surf = pygame.Surface(self.dims, depth=8)
dims = (disp_width, size / disp_width)
self.init_surface(pygame.Surface(dims, depth=8))
self.surf.set_palette(palette) self.surf.set_palette(palette)
# label surface def render(self, cpu, frame_time_ms):
self.surf_labels = pygame.Surface((2 * 8 + self.tick_size, size * 8 * 2), depth=24) w, h = self.dims()
mem = cpu.memory[self.start_addr:self.start_addr + (w * h)]
self.width = self.surf.get_width() * SURF_SCALE arr = numpy.reshape(mem, (h, w))
self.width += self.surf_labels.get_width()
self.height = self.surf.get_height()
self.surf.fill((0, 0, 0))
self.surf_labels.fill((0, 0, 0))
# build labels
for i in range(0, size):
y = i * 8 * SURF_SCALE
chargen_draw_str(self.surf_labels, 0, y, '%02X' % i, color=self.label_color)
# draw ticks
for t in range(0, self.tick_size):
self.surf_labels.set_at((16 + t, y), self.tick_color)
def scale(self, f):
return pygame.transform.scale(self.surf, (int(self.surf.get_width() * f), int(self.surf.get_height() * f)))
def render(self, cpu):
w, h = self.dims
arr = numpy.reshape(cpu.memory[self.start_addr:(w * h)], (h, w))
pygame.surfarray.blit_array(self.surf, numpy.transpose(arr)) pygame.surfarray.blit_array(self.surf, numpy.transpose(arr))
def blit(self, screen, pos, scale=1):
x, y = pos
screen.blit(self.surf_labels, pos)
x += self.surf_labels.get_width()
screen.blit(self.scale(SURF_SCALE), [x, y])
return [x + self.width, y + self.height]

View file

@ -0,0 +1,36 @@
from .heatmap import MemHeatmap
from .access_map import AccessMap
from .text_label import TextLabel
class MicroMemView:
mem_map = None
read_map = None
write_map = None
def __init__(self, size=0x1000, start_addr=0, caption='Unnamed', disp_width=256):
self.mem_map = MemHeatmap(start_addr=start_addr, size=size, disp_width=disp_width)
self.mem_map.surf.set_alpha(128)
self.caption = TextLabel("%04X-%04X: %s (%d bpl)" % (start_addr, start_addr+size-1, caption, disp_width))
self.read_map = AccessMap(start_addr=start_addr, size=size, disp_width=disp_width, color=(0,128+64,255))
self.write_map = AccessMap(start_addr=start_addr, size=size, disp_width=disp_width)
self.width = self.mem_map.width
self.height = self.mem_map.height
def render(self, cpu, frame_time_ms):
self.mem_map.render(cpu, frame_time_ms)
self.read_map.render(cpu, frame_time_ms)
self.write_map.render(cpu, frame_time_ms)
def blit(self, screen, pos, scale=1):
x,y = pos
_,y = self.caption.blit(screen, pos)
self.mem_map.blit(screen, (x,y), scale=scale)
self.read_map.blit(screen, (x,y), scale=scale)
x, y = self.write_map.blit(screen, (x,y), scale=scale)
return [x, y]
def listen(self, cpu):
cpu.store_mem_listeners.append(self.write_map.mem_listener)
cpu.read_mem_listeners.append(self.read_map.mem_listener)

9
orao/views/text_label.py Normal file
View file

@ -0,0 +1,9 @@
import pygame
from .view import View
from ..chargen import chargen_draw_str
class TextLabel(View):
def __init__(self, text):
self.init_surface(pygame.Surface((len(text)*8, 8), depth=24))
chargen_draw_str(self.surf, 0,0, text, color=(0xff, 0xcc, 0x00))

31
orao/views/view.py Normal file
View file

@ -0,0 +1,31 @@
import pygame
class View:
surf = None
scale_method = pygame.transform.scale
def init_surface(self, surf):
self.surf = surf
self.width = self.surf.get_width()
self.height = self.surf.get_height()
def set_smooth_scale(self):
self.scale_method = pygame.transform.smoothscale
def scaled_surf(self, f):
return self.scale_method(self.surf, (int(self.width * f), int(self.surf.get_height() * f)))
def dims(self):
return self.width, self.height
def blit(self, screen, pos, scale=1):
if self.surf is None:
return pos
# if scale == 1:
# screen.blit(self.surf, pos)
# else:
screen.blit(self.scaled_surf(scale), pos)
x, y = pos
return [int(x + self.width * scale), int(y + self.height * scale)]