"""
SimpleVGA is very basic implementation of VGA-like device, with text
and graphic modes.
"""
import array
import enum
import functools
from six.moves import range
from ctypes import c_ushort, LittleEndianStructure
from . import Device, MMIOMemoryPage
from ..errors import InvalidResourceError
from ..mm import PAGE_SIZE, ExternalMemoryPage, addr_to_page, u8_t
from ..util import sizeof_fmt, F, UINT16_FMT, UINT32_FMT, UINT8_FMT
from ..reactor import RunInIntervalTask
from ..streams import OutputStream
#: Default memory size, in bytes
DEFAULT_MEMORY_SIZE = 64 * 1024
#: Default number of memory banks
DEFAULT_MEMORY_BANKS = 8
#: Default MMIO address
DEFAULT_MMIO_ADDRESS = 0x8100
[docs]class SimpleVGAPorts(enum.IntEnum):
CONTROL = 0x00
DATA = 0x02
[docs]class SimpleVGACommands(enum.IntEnum):
RESET = 0x0001
REFRESH = 0x0002
GRAPHIC = 0x0020
COLS = 0x0021
ROWS = 0x0022
DEPTH = 0x0023
MEMORY_BANK_ID = 0x0030
[docs]class Mode(object):
def __init__(self, _type, width, height, depth):
self.type = _type
self.width = width
self.height = height
self.depth = depth
self.required_memory = self.width * self.height * (self.depth if self.type == 't' else self.depth // 8)
def __cmp__(self, other):
return self.type == other.type and self.width == other.width and self.height == other.height and self.depth == other.depth
def __eq__(self, other):
return self.__cmp__(other)
def __repr__(self):
return self.to_string()
@classmethod
[docs] def from_string(cls, s):
"""
Create ``Mode`` object from its string representation. It's a comma-separated
list of for items:
+--------------+----------+-----------------+-----------------------+-----------------+
| | ``type`` | ``width`` | ``height`` | ``depth`` |
+--------------+----------+-----------------+-----------------------+-----------------+
| Text mode | ``t`` | chars per line | lines on screen | bytes per char |
+--------------+----------+-----------------+-----------------------+-----------------+
| Graphic mode | ``g`` | pixels per line | pixel lines on screen | bites per pixel |
+--------------+----------+-----------------+-----------------------+-----------------+
"""
t, c, r, b = s.strip().split(',')
return cls(t, int(c.strip()), int(r.strip()), int(b.strip()))
[docs] def to_string(self):
return F('({type}, {width}, {height}, {depth})', type = self.type, width = self.width, height = self.height, depth = self.depth)
[docs] def to_pretty_string(self):
return F('{type}, {cols}x{rows} {entities}, {memory_per_entity} {memory_label}',
type = 'text' if self.type == 't' else 'graphic',
cols = self.width,
rows = self.height,
entities = 'chars' if self.type == 't' else 'pixels',
memory_per_entity = self.depth if self.type == 't' else self.depth * 8,
memory_label = 'bytes per char' if self.type == 't' else 'bits color depth'
)
#: Default list of available modes
DEFAULT_MODES = [
Mode('g', 320, 200, 1),
Mode('t', 80, 25, 2),
Mode('t', 80, 25, 1)
]
#: Default boot mode
DEFAULT_BOOT_MODE = Mode('t', 80, 25, 1)
[docs]class Char(LittleEndianStructure):
_pack_ = 0
_fields_ = [
('codepoint', c_ushort, 7),
('unused', c_ushort, 1),
('fg', c_ushort, 4),
('bg', c_ushort, 3),
('blink', c_ushort, 1)
]
def __repr__(self):
return '<Char: cp=%s, fg=%s, bg=%s, bl=%s>' % (self.codepoint, self.fg, self.bg, self.blink)
[docs] def to_u8(self):
return (u8_t(self.codepoint | self.unused << 7).value, u8_t(self.fg | self.bg << 4 | self.blink << 7).value)
@staticmethod
[docs] def from_u8(l, h):
c = Char()
c.codepoint = l & 0x7F
c.fg = h & 0x0F
c.bg = (h >> 4) & 0x07
c.blink = h >> 7
return c
@staticmethod
[docs] def from_u16(u):
return Char.from_u8(u & 0x00FF, u >> 8)
[docs]class DisplayRefreshTask(RunInIntervalTask):
def __init__(self, display):
super(DisplayRefreshTask, self).__init__(200, self.on_tick)
self.display = display
self.first_tick = True
self.write = display.stream_out.write
[docs] def on_tick(self, task):
self.display.machine.DEBUG('Display: refresh display')
gpu = self.display.gpu
mode = gpu.active_mode
palette_fg = [30, 34, 32, 36, 31, 35, 31, 37, 90, 94, 92, 96, 91, 95, 33, 97]
palette_bg = [40, 44, 42, 46, 41, 45, 41, 47, 100, 104, 102, 106, 101, 105, 43, 107]
if mode.type == 't':
screen = []
if mode.depth not in (1, 2):
self.display.machine.WARN(F('Unhandled character depth: mode={mode}', mode = self.active_mode))
return
for row in range(0, mode.height):
line = []
for col in range(0, mode.width):
if mode.depth == 1:
c = Char.from_u8(gpu.memory[row * mode.width + col], 0)
c.fg = 15
c.bg = 0
else:
c = Char.from_u8(gpu.memory[(row * mode.width + col) * 2], gpu.memory[(row * mode.width + col) * 2 + 1])
self.display.machine.DEBUG(' c=%s', c)
char = F('\033[{blink:d};{fg:d};{bg:d}m{char}\033[0m',
blink = 5 if c.blink == 1 else 0,
fg = palette_fg[c.fg],
bg = palette_bg[c.bg],
char = ' ' if c.codepoint == 0 else chr(c.codepoint)
)
line.append(char)
screen.append(''.join(line))
def __line(l):
return [ord(c) for c in l] + [ord('\n')]
if self.first_tick:
self.first_tick = False
else:
self.write(__line(F('\033[{rows:d}F', rows = mode.height + 3)))
self.write(__line('-' * mode.width))
for line in screen:
self.write(__line(line))
self.write(__line('-' * mode.width))
else:
self.display.machine.WARN(F('Unhandled gpu mode: mode={mode}', mode = gpu.active_mode))
[docs]class Display(Device):
def __init__(self, machine, name, gpu = None, stream_out = None, *args, **kwargs):
super(Display, self).__init__(machine, 'display', name, *args, **kwargs)
self.gpu = gpu
self.stream_out = stream_out
self.gpu.master = self
self.refresh_task = DisplayRefreshTask(self)
@staticmethod
[docs] def get_slave_gpu(machine, config, section):
gpu_name = config.get(section, 'gpu', None)
gpu_device = machine.get_device_by_name(gpu_name)
if not gpu_name or not gpu_device:
raise InvalidResourceError(F('Unknown GPU device: gpu={name}', name = gpu_name))
return gpu_device
@staticmethod
[docs] def create_from_config(machine, config, section):
gpu = Display.get_slave_gpu(machine, config, section)
stream_out = OutputStream.create(machine, config.get(section, 'stream_out', '<stdout>'))
return Display(machine, section, gpu = gpu, stream_out = stream_out)
[docs] def boot(self):
self.machine.DEBUG('Display.boot')
super(Display, self).boot()
self.gpu.boot()
self.machine.reactor.add_task(self.refresh_task)
self.machine.reactor.task_runnable(self.refresh_task)
self.machine.tenh(F('display: generic {name} connected to gpu {gpu}, output stream {stream}', name = self.name, gpu = self.gpu.name, stream = self.stream_out))
[docs] def halt(self):
self.machine.DEBUG('Display.halt')
super(Display, self).halt()
self.machine.reactor.remove_task(self.refresh_task)
self.gpu.halt()
self.machine.DEBUG('Display: halted')
[docs]class SimpleVGAMemoryPage(ExternalMemoryPage):
"""
Memory page handling MMIO of sVGA device.
:param ducky.devices.svga.SimpleVGA dev: sVGA device this page belongs to.
"""
def __init__(self, dev, *args, **kwargs):
super(SimpleVGAMemoryPage, self).__init__(*args, **kwargs)
self.dev = dev
[docs] def get(self, offset):
return self.data[self.dev.bank_offsets[self.dev.active_bank] + self.offset + offset]
[docs] def put(self, offset, b):
self.data[self.dev.bank_offsets[self.dev.active_bank] + self.offset + offset] = b
[docs]class SimpleVGAMMIOMemoryPage(MMIOMemoryPage):
[docs] def read_u16(self, offset):
self.DEBUG('%s.read_u16: offset=%s', self.__class__.__name__, offset)
if offset == SimpleVGAPorts.CONTROL:
return 0x0000
if offset == SimpleVGAPorts.DATA:
state, self._device.state = self._device.state, None
if state == SimpleVGACommands.GRAPHIC:
return 1 if self._device.active_mode.type == 'g' else 0
if state == SimpleVGACommands.COLS:
return self._device.active_mode.width
if state == SimpleVGACommands.ROWS:
return self._device.active_mode.height
if state == SimpleVGACommands.DEPTH:
return self._device.active_mode.depth
if state == SimpleVGACommands.MEMORY_BANK_ID:
return self._device.active_bank
self.WARN('%s.get: unknown state: state=%s', self.__class__.__name__, state)
return 0xFFFF
self.WARN('%s.get: attempt to read raw offset: offset=%s', self.__class__.__name__, offset)
return 0x0000
[docs] def write_u16(self, offset, value):
self.DEBUG('%s.put: offset=%s, value=%s', self.__class__.__name__, UINT8_FMT(offset), UINT16_FMT(value))
if offset == SimpleVGAPorts.CONTROL:
if value == SimpleVGACommands.RESET:
self._device.reset()
return
if value == SimpleVGACommands.REFRESH:
self._device.master.refresh_task.on_tick(None)
return
self._device.state = value
return
if offset == SimpleVGAPorts.DATA:
state, self._device.state = self._device.state, None
if state == SimpleVGACommands.MEMORY_BANK_ID:
if not 0 <= value < self._device.memory_banks:
raise InvalidResourceError(F('Memory bank out of range: bank={bank:d}', bank = value))
self._device.active_bank = value
return
self.WARN('%s.put: unknown state: state=%s', self.__class__.__name__, state)
return
self.WARN('%s.put: attempt to write raw offset: offset=%s', self.__class__.__name__, offset)
[docs]class SimpleVGA(Device):
"""
SimpleVGA is very basic implementation of VGA-like device, with text
and graphic modes.
It has its own graphic memory ("buffer"), split into several banks of
the same size. Always only one bank can be directly accessed, by having
it mapped into CPU's address space.
:param ducky.machine.Machine machine: machine this device belongs to.
:param string name: name of this device.
:param int memory_size: size of graphic memory.
:param u32_t mmio_address: base oddress of MMIO ports.
:param u24 memory_address: address of graphic memory - to this address is
graphic buffer mapped. Must be specified, there is no default value.
:param int memory_banks: number of memory banks.
:param list modes: list of :py:class:`ducky.devices.svga.Mode` objects,
list of supported modes.
:param tuple boot_mode: this mode will be set when device boots up.
"""
def __init__(self, machine, name, memory_size = None, mmio_address = None, memory_address = None, memory_banks = None, modes = None, boot_mode = None, *args, **kwargs):
if memory_address is None:
raise InvalidResourceError('sVGA device memory address must be specified explicitly')
if memory_address % PAGE_SIZE:
raise InvalidResourceError('sVGA device memory address must be page-aligned')
super(SimpleVGA, self).__init__(machine, 'gpu', name, *args, **kwargs)
self.memory_size = memory_size or DEFAULT_MEMORY_SIZE
self.memory_address = memory_address
self.memory_banks = memory_banks or DEFAULT_MEMORY_BANKS
self.modes = modes or DEFAULT_MODES
self._mmio_address = mmio_address or DEFAULT_MMIO_ADDRESS
self._mmio_page = None
if self.memory_size % PAGE_SIZE:
raise InvalidResourceError('sVGA device memory size must be page-aligned')
if (self.memory_size // self.memory_banks) % PAGE_SIZE:
raise InvalidResourceError('sVGA device memory bank size must be page-aligned')
self.active_mode = None
self.boot_mode = boot_mode or DEFAULT_BOOT_MODE
if self.boot_mode not in self.modes:
raise InvalidResourceError(F('Boot mode not available: boot_mode={mode}, modes={modes}', mode = self.boot_mode, modes = self.modes))
for mode in self.modes:
if mode.required_memory > self.memory_size:
raise InvalidResourceError(F('Not enough memory for mode: mode={mode}, required={bytes_required:d} bytes, available={bytes_available:d} bytes', mode = mode, bytes_required = mode.required_memory, bytes_available = self.memory_size))
self.memory = self.data = array.array('B', [0 for _ in range(0, self.memory_size)])
self.bank_offsets = list(range(0, self.memory_size, self.memory_size // self.memory_banks))
self.pages_per_bank = self.memory_size // PAGE_SIZE // self.memory_banks
self.machine.DEBUG(F('sVGA: memory-size={memory_size:d}, memory-banks={memory_banks:d}, offsets=[{bank_offsets}], pages-per-bank={pages_per_bank:d}, address={address:W}', memory_size = self.memory_size, memory_banks = self.memory_banks, bank_offsets = ', '.join([UINT32_FMT(o) for o in self.bank_offsets]), pages_per_bank = self.pages_per_bank, address = self.memory_address))
@staticmethod
[docs] def create_from_config(machine, config, section):
_getint = functools.partial(config.getint, section)
def parse_mode(m):
t, c, r, b = m.strip().split(',')
return (t, int(c.strip()), int(r.strip()), int(b.strip()))
modes = config.get(section, 'modes', None)
if modes is not None:
modes = [Mode.from_string(m) for m in modes.split(';')]
boot_mode = config.get(section, 'boot-mode', None)
if boot_mode is not None:
boot_mode = Mode.from_string(boot_mode)
return SimpleVGA(machine, section,
memory_size = _getint('memory-size', DEFAULT_MEMORY_SIZE),
memory_address = _getint('memory-address', None),
memory_banks = _getint('memory-banks', DEFAULT_MEMORY_BANKS),
mmio_address = _getint('mmio-address', DEFAULT_MMIO_ADDRESS),
modes = modes,
boot_mode = boot_mode)
def __repr__(self):
return F('sVGA adapter {name} ({memory_size} VRAM in {memory_banks:d} banks at {memory_address}, control [{mmio}]; {mode} mode)',
name = self.name,
memory_size = sizeof_fmt(self.memory_size),
memory_banks = self.memory_banks,
memory_address = UINT32_FMT(self.memory_address),
mmio = UINT32_FMT(self._mmio_address),
mode = self.active_mode.to_pretty_string() if self.active_mode is not None else (self.boot_mode.to_pretty_string() + ' boot')
)
[docs] def reset(self):
self.state = None
self.active_mode = None
self.active_bank = 0
for pg in self.pages:
pg.clear()
[docs] def set_mode(self, mode):
self.active_mode = mode
[docs] def boot(self):
self.machine.DEBUG('SimpleVGA.boot')
self.pages = []
pages_start = addr_to_page(self.memory_address)
for i in range(pages_start, pages_start + self.pages_per_bank):
pg = SimpleVGAMemoryPage(self, self.machine.memory, i, self.memory, offset = (i - pages_start) * PAGE_SIZE)
self.machine.memory.register_page(pg)
self.pages.append(pg)
self._mmio_page = SimpleVGAMMIOMemoryPage(self, self.machine.memory, addr_to_page(self._mmio_address))
self.machine.memory.register_page(self._mmio_page)
self.reset()
self.set_mode(self.boot_mode)
self.machine.tenh(F('gpu: {gpu}', gpu = self))
[docs] def halt(self):
self.machine.DEBUG('SimpleVGA.halt')
for pg in self.pages:
self.machine.memory.unregister_page(pg)
self.pages = []
self.machine.memory.unregister_page(self._mmio_page)
self.machine.DEBUG('SimpleVGA: halted')