refactor: improve UI rendering, show access heatmap
This commit is contained in:
parent
63c03e55fa
commit
0d3c3c28a0
9 changed files with 216 additions and 90 deletions
|
@ -1,7 +1,7 @@
|
|||
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
|
||||
computer used primarily in elementary schools, as part of a computer literacy
|
||||
|
|
BIN
assets/screenshot-20211017-225446.png
Normal file
BIN
assets/screenshot-20211017-225446.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
79
orao.py
79
orao.py
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/python2
|
||||
# -*- coding: utf8 -*-
|
||||
|
||||
import pygame, numpy, sys, datetime, wave, time
|
||||
import pygame, numpy, sys, datetime
|
||||
from orao.cpu import CPU
|
||||
from orao.keyboard import listener as orao_kbd_listener
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -36,51 +36,82 @@ chargen_init(cpu.memory[0xE000:])
|
|||
|
||||
# views
|
||||
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_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))
|
||||
chargen_draw_str(status_line, 0, 0, 'Orao Emulator v0.1')
|
||||
|
||||
# setup screen
|
||||
screen = pygame.display.set_mode((
|
||||
terminal.get_width() * 2 + 1 + int(max(view_heatmap.width, view_cpu_state.width*1.8)),
|
||||
terminal.get_height() * 2 + 3*8 + 2
|
||||
terminal.get_width() * 2 + 2 + int(max(view_rom.width, view_cpu_state.width * 2)),
|
||||
terminal.get_height() * 2 + 3*8 + 2 + 30
|
||||
))
|
||||
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:
|
||||
chargen_draw_str(status_line, 0, 16, 'F8:', color=(0, 0, 0), bg=(0, 255, 0))
|
||||
chargen_draw_str(status_line, 24, 16, ' %s' % MEM_LOAD_PRG)
|
||||
chargen_draw_str(status_line, 0, 24, 'F8:', color=lc)
|
||||
chargen_draw_str(status_line, 24, 24, ' %s' % MEM_LOAD_PRG)
|
||||
|
||||
def render_frame():
|
||||
view_cpu_state.render(cpu)
|
||||
view_heatmap.render(cpu)
|
||||
def render_frame(frame_time_ms):
|
||||
view_cpu_state.render(cpu, frame_time_ms)
|
||||
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
|
||||
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))
|
||||
screen.blit(status_line, [0, 512+1])
|
||||
lsx = 512 + 1
|
||||
lsy = 0
|
||||
_, lsy = view_cpu_state.blit(screen, [lsx, lsy], scale=1.8)
|
||||
x = 512 + 1
|
||||
y = 0
|
||||
cx, y = view_cpu_state.blit(screen, [x, y], scale=2)
|
||||
|
||||
lsy += 1
|
||||
view_heatmap.blit(screen, [lsx, lsy])
|
||||
y += 5
|
||||
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
|
||||
pygame.display.flip()
|
||||
|
||||
clock = pygame.time.Clock()
|
||||
|
||||
while running:
|
||||
before, previous_loop_cycles = datetime.datetime.now(), cpu.cycles
|
||||
time_elapsed = lambda: (datetime.datetime.now()-before).microseconds + 1
|
||||
|
||||
for i in range(5000):
|
||||
cpu.step()
|
||||
|
||||
time_elapsed = (datetime.datetime.now()-before).microseconds + 1
|
||||
clock.tick()
|
||||
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
|
@ -117,21 +148,25 @@ while running:
|
|||
# HACK: reset stack pointer
|
||||
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:
|
||||
render_frame()
|
||||
render_frame(clock.get_time())
|
||||
|
||||
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:
|
||||
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.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
|
||||
|
||||
ratio = 1.0 * (cpu.cycles - previous_loop_cycles) / time_elapsed()
|
||||
ratio = 1.0 * (cpu.cycles - previous_loop_cycles) / time_elapsed
|
||||
|
||||
pygame.quit()
|
||||
|
|
53
orao/views/access_map.py
Normal file
53
orao/views/access_map.py
Normal 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))
|
|
@ -1,35 +1,26 @@
|
|||
import pygame
|
||||
from .view import View
|
||||
from ..chargen import chargen_draw_str
|
||||
|
||||
class CPUState:
|
||||
surf = None
|
||||
|
||||
class CPUState(View):
|
||||
|
||||
def __init__(self):
|
||||
self.surf = pygame.Surface((8 * 8, 6 * 8), depth=24)
|
||||
self.width = self.surf.get_width()
|
||||
self.height = self.surf.get_height()
|
||||
self.surf.fill((0, 0, 0))
|
||||
self.init_surface(pygame.Surface((2 * 8 * 8, 3 * 8), depth=24))
|
||||
#self.set_smooth_scale()
|
||||
|
||||
def scale(self, f):
|
||||
return pygame.transform.smoothscale(self.surf, (int(self.width * f), int(self.height * f)))
|
||||
def draw_text(self, x, y, text, color=(0, 255, 0)):
|
||||
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)
|
||||
chargen_draw_str(self.surf, 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, 0 * 8, 'A X Y', color=lc)
|
||||
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)
|
||||
chargen_draw_str(self.surf, 0, 3*8, 'SP:', color=lc)
|
||||
chargen_draw_str(self.surf, 4*8, 2*8, '%04X' % cpu.pc)
|
||||
chargen_draw_str(self.surf, 4*8, 3*8, '%04X' % cpu.sp)
|
||||
self.draw_text(0, 2 * 8, 'PC:', color=lc)
|
||||
self.draw_text(8 * 8, 2 * 8, 'SP:', color=lc)
|
||||
self.draw_text(3 * 8, 2 * 8, '%04X' % cpu.pc)
|
||||
self.draw_text(11 * 8, 2 * 8, '%04X' % cpu.sp)
|
||||
|
||||
chargen_draw_str(self.surf, 0, 4*8, "NVssDIZC", color=lc)
|
||||
chargen_draw_str(self.surf, 0, 5*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))
|
||||
self.draw_text(8 * 8, 0 * 8, "NVssDIZC", color=lc)
|
||||
self.draw_text(8 * 8, 1 * 8, "{0:b}".format(cpu.flags))
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import pygame
|
||||
import numpy
|
||||
from ..chargen import chargen_draw_str
|
||||
|
||||
SURF_SCALE = 2
|
||||
from .view import View
|
||||
|
||||
palette = []
|
||||
# below chars
|
||||
|
@ -12,51 +10,24 @@ palette += [(i * 2, 255, 0) for i in range(128 - 32)]
|
|||
# other
|
||||
palette += [(i * 2, 0, 255) for i in range(128)]
|
||||
|
||||
# displays memory locations as colors of palette based on value
|
||||
|
||||
class MemHeatmap:
|
||||
dims = (32, 192)
|
||||
class MemHeatmap(View):
|
||||
surf = None
|
||||
tick_color = (0xff, 0xcc, 0x00)
|
||||
label_color = (0xff, 0xcc, 0x00)
|
||||
tick_size = 2
|
||||
start_addr = 0
|
||||
size = 0
|
||||
|
||||
def __init__(self, size=16, start_addr=0):
|
||||
# mem view surface
|
||||
def __init__(self, size=0x1000, start_addr=0, disp_width=256):
|
||||
self.start_addr = start_addr
|
||||
self.dims = (32, size * 8)
|
||||
self.surf = pygame.Surface(self.dims, depth=8)
|
||||
self.size = size
|
||||
|
||||
dims = (disp_width, size / disp_width)
|
||||
self.init_surface(pygame.Surface(dims, depth=8))
|
||||
self.surf.set_palette(palette)
|
||||
|
||||
# label surface
|
||||
self.surf_labels = pygame.Surface((2 * 8 + self.tick_size, size * 8 * 2), depth=24)
|
||||
|
||||
self.width = self.surf.get_width() * SURF_SCALE
|
||||
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))
|
||||
def render(self, cpu, frame_time_ms):
|
||||
w, h = self.dims()
|
||||
mem = cpu.memory[self.start_addr:self.start_addr + (w * h)]
|
||||
arr = numpy.reshape(mem, (h, w))
|
||||
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]
|
||||
|
|
36
orao/views/micro_mem_view.py
Normal file
36
orao/views/micro_mem_view.py
Normal 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
9
orao/views/text_label.py
Normal 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
31
orao/views/view.py
Normal 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)]
|
Loading…
Reference in a new issue