Python: Revamp a bunch of stuff

This commit is contained in:
Vicki Pfau 2018-06-25 15:38:31 -07:00
parent 3f05b12bc1
commit 7fa8de1f0d
27 changed files with 632 additions and 487 deletions

View File

@ -0,0 +1,2 @@
[MESSAGES CONTROL]
disable=line-too-long,missing-docstring,too-few-public-methods,too-many-instance-attributes,too-many-public-methods,wrong-import-order

View File

@ -11,15 +11,6 @@ endforeach()
file(GLOB PYTHON_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
if(NOT GIT_TAG)
if(GIT_BRANCH STREQUAL "master" OR NOT GIT_BRANCH)
set(PYLIB_VERSION -b .dev${GIT_REV}+g${GIT_COMMIT_SHORT})
else()
set(PYLIB_VERSION -b .dev${GIT_REV}+${GIT_BRANCH}.g${GIT_COMMIT_SHORT})
endif()
endif()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in ${CMAKE_CURRENT_BINARY_DIR}/setup.py)
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/lib.c
COMMAND BINDIR=${CMAKE_CURRENT_BINARY_DIR}/.. CPPFLAGS="${INCLUDE_FLAGS}" ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/_builder.py
COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/lib.c
@ -35,15 +26,29 @@ set_target_properties(${BINARY_NAME}-pylib PROPERTIES INCLUDE_DIRECTORIES "${CMA
set_target_properties(${BINARY_NAME}-pylib PROPERTIES COMPILE_DEFINITIONS "${OS_DEFINES};${FEATURE_DEFINES};${FUNCTION_DEFINES}")
add_custom_target(${BINARY_NAME}-py ALL
COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/setup.py egg_info -e ${CMAKE_CURRENT_BINARY_DIR} ${PYLIB_VERSION}
COMMAND BINDIR=${CMAKE_CURRENT_BINARY_DIR}/.. CPPFLAGS="${INCLUDE_FLAGS}" ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/setup.py build -b ${CMAKE_CURRENT_BINARY_DIR}
COMMAND BINDIR=${CMAKE_CURRENT_BINARY_DIR}/.. LIBDIR=${CMAKE_CURRENT_BINARY_DIR}/.. CPPFLAGS="${INCLUDE_FLAGS}" ${PYTHON_EXECUTABLE} setup.py build -b ${CMAKE_CURRENT_BINARY_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${BINARY_NAME}
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/setup.py
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/setup.py
DEPENDS ${PYTHON_HEADERS}
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/_builder.py
DEPENDS ${BINARY_NAME}-pylib)
add_custom_target(${BINARY_NAME}-py-install
COMMAND BINDIR=${CMAKE_CURRENT_BINARY_DIR}/.. LIBDIR=${CMAKE_CURRENT_BINARY_DIR}/.. CPPFLAGS="${INCLUDE_FLAGS}" ${PYTHON_EXECUTABLE} setup.py install -b ${CMAKE_CURRENT_BINARY_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${BINARY_NAME}-py)
add_custom_target(${BINARY_NAME}-py-develop
COMMAND BINDIR=${CMAKE_CURRENT_BINARY_DIR}/.. LIBDIR=${CMAKE_CURRENT_BINARY_DIR}/.. CPPFLAGS="${INCLUDE_FLAGS}" ${PYTHON_EXECUTABLE} setup.py develop -b ${CMAKE_CURRENT_BINARY_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${BINARY_NAME}-py)
add_custom_target(${BINARY_NAME}-py-bdist
COMMAND BINDIR=${CMAKE_CURRENT_BINARY_DIR}/.. LIBDIR=${CMAKE_CURRENT_BINARY_DIR}/.. CPPFLAGS="${INCLUDE_FLAGS}" ${PYTHON_EXECUTABLE} setup.py bdist_wheel -b ${CMAKE_CURRENT_BINARY_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${BINARY_NAME}-py)
file(GLOB BASE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_*.py)
file(GLOB SUBTESTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/*/test_*.py)
foreach(TEST IN LISTS BASE_TESTS SUBTESTS)
@ -56,6 +61,8 @@ foreach(TEST IN LISTS BASE_TESTS SUBTESTS)
endif()
string(REGEX REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/(tests/.*/)?test_" "" TEST_NAME "${TEST}")
string(REPLACE ".py" "" TEST_NAME "${TEST_NAME}")
add_test(python-${TEST_NAME} ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/setup.py build -b ${CMAKE_CURRENT_BINARY_DIR} pytest --extras --addopts ${TEST})
set_tests_properties(python-${TEST_NAME} PROPERTIES ENVIRONMENT "${PATH}=${CMAKE_CURRENT_BINARY_DIR}/..")
add_test(NAME python-${TEST_NAME}
COMMAND ${PYTHON_EXECUTABLE} setup.py build -b ${CMAKE_CURRENT_BINARY_DIR} pytest --extras --addopts ${TEST}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
set_tests_properties(python-${TEST_NAME} PROPERTIES ENVIRONMENT "${PATH}=${CMAKE_CURRENT_BINARY_DIR}/..;BINDIR=${CMAKE_CURRENT_BINARY_DIR}/..;LIBDIR=${CMAKE_CURRENT_BINARY_DIR}/..;CPPFLAGS=${INCLUDE_FLAGS}")
endforeach()

View File

@ -78,10 +78,6 @@ ffi.embedding_api('\n'.join(lines))
ffi.embedding_init_code("""
import os, os.path
venv = os.getenv('VIRTUAL_ENV')
if venv:
activate = os.path.join(venv, 'bin', 'activate_this.py')
exec(compile(open(activate, "rb").read(), activate, 'exec'), dict(__file__=activate))
from mgba._pylib import ffi, lib
symbols = {}
globalSyms = {
@ -109,7 +105,7 @@ ffi.embedding_init_code("""
from mgba.vfs import VFile
vf = VFile(vf)
name = ffi.string(name)
source = vf.readAll().decode('utf-8')
source = vf.read_all().decode('utf-8')
try:
code = compile(source, name, 'exec')
pendingCode.append(code)

View File

@ -2,15 +2,16 @@ from PIL.ImageChops import difference
from PIL.ImageOps import autocontrast
from PIL.Image import open as PIOpen
class VideoFrame(object):
def __init__(self, pilImage):
self.image = pilImage.convert('RGB')
def __init__(self, pil_image):
self.image = pil_image.convert('RGB')
@staticmethod
def diff(a, b):
diff = difference(a.image, b.image)
diffNormalized = autocontrast(diff)
return (VideoFrame(diff), VideoFrame(diffNormalized))
diff_normalized = autocontrast(diff)
return (VideoFrame(diff), VideoFrame(diff_normalized))
@staticmethod
def load(path):

View File

@ -4,44 +4,38 @@ from . import VideoFrame
Output = namedtuple('Output', ['video'])
class Tracer(object):
def __init__(self, core):
self.core = core
self.fb = Image(*core.desiredVideoDimensions())
self.core.setVideoBuffer(self.fb)
self._videoFifo = []
self.framebuffer = Image(*core.desired_video_dimensions())
self.core.set_video_buffer(self.framebuffer)
self._video_fifo = []
def yieldFrames(self, skip=0, limit=None):
def yield_frames(self, skip=0, limit=None):
self.core.reset()
skip = (skip or 0) + 1
while skip > 0:
frame = self.core.frameCounter
self.core.runFrame()
frame = self.core.frame_counter
self.core.run_frame()
skip -= 1
while frame <= self.core.frameCounter and limit != 0:
self._videoFifo.append(VideoFrame(self.fb.toPIL()))
while frame <= self.core.frame_counter and limit != 0:
self._video_fifo.append(VideoFrame(self.framebuffer.to_pil()))
yield frame
frame = self.core.frameCounter
self.core.runFrame()
frame = self.core.frame_counter
self.core.run_frame()
if limit is not None:
assert limit >= 0
limit -= 1
def video(self, generator=None, **kwargs):
if not generator:
generator = self.yieldFrames(**kwargs)
generator = self.yield_frames(**kwargs)
try:
while True:
if self._videoFifo:
result = self._videoFifo[0]
self._videoFifo = self._videoFifo[1:]
yield result
if self._video_fifo:
yield self._video_fifo.pop(0)
else:
next(generator)
except StopIteration:
return
def output(self, **kwargs):
generator = self.yieldFrames(**kwargs)
return mCoreOutput(video=self.video(generator=generator, **kwargs))

View File

@ -1,5 +1,7 @@
import os, os.path
import mgba.core, mgba.image
import os
import os.path
import mgba.core
import mgba.image
import cinema.movie
import itertools
import glob
@ -7,20 +9,21 @@ import re
import yaml
from copy import deepcopy
from cinema import VideoFrame
from cinema.util import dictMerge
from cinema.util import dict_merge
class CinemaTest(object):
TEST = 'test.(mvl|gb|gba|nds)'
def __init__(self, path, root, settings={}):
self.fullPath = path or []
self.path = os.path.abspath(os.path.join(root, *self.fullPath))
self.full_path = path or []
self.path = os.path.abspath(os.path.join(root, *self.full_path))
self.root = root
self.name = '.'.join(path)
self.settings = settings
try:
with open(os.path.join(self.path, 'manifest.yml'), 'r') as f:
dictMerge(self.settings, yaml.safe_load(f))
dict_merge(self.settings, yaml.safe_load(f))
except IOError:
pass
self.tests = {}
@ -28,41 +31,42 @@ class CinemaTest(object):
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, self.name)
def setUp(self):
def setup(self):
results = [f for f in glob.glob(os.path.join(self.path, 'test.*')) if re.search(self.TEST, f)]
self.core = mgba.core.loadPath(results[0])
self.core = mgba.core.load_path(results[0])
if 'config' in self.settings:
self.config = mgba.core.Config(defaults=self.settings['config'])
self.core.loadConfig(self.config)
self.core.load_config(self.config)
self.core.reset()
def addTest(self, name, cls=None, settings={}):
def add_test(self, name, cls=None, settings={}):
cls = cls or self.__class__
newSettings = deepcopy(self.settings)
dictMerge(newSettings, settings)
self.tests[name] = cls(self.fullPath + [name], self.root, newSettings)
new_settings = deepcopy(self.settings)
dict_merge(new_settings, settings)
self.tests[name] = cls(self.full_path + [name], self.root, new_settings)
return self.tests[name]
def outputSettings(self):
outputSettings = {}
def output_settings(self):
output_settings = {}
if 'frames' in self.settings:
outputSettings['limit'] = self.settings['frames']
output_settings['limit'] = self.settings['frames']
if 'skip' in self.settings:
outputSettings['skip'] = self.settings['skip']
return outputSettings
output_settings['skip'] = self.settings['skip']
return output_settings
def __lt__(self, other):
return self.path < other.path
class VideoTest(CinemaTest):
BASELINE = 'baseline_%04u.png'
def setUp(self):
super(VideoTest, self).setUp();
def setup(self):
super(VideoTest, self).setup()
self.tracer = cinema.movie.Tracer(self.core)
def generateFrames(self):
for i, frame in zip(itertools.count(), self.tracer.video(**self.outputSettings())):
def generate_frames(self):
for i, frame in zip(itertools.count(), self.tracer.video(**self.output_settings())):
try:
baseline = VideoFrame.load(os.path.join(self.path, self.BASELINE % i))
yield baseline, frame, VideoFrame.diff(baseline, frame)
@ -70,14 +74,15 @@ class VideoTest(CinemaTest):
yield None, frame, (None, None)
def test(self):
self.baseline, self.frames, self.diffs = zip(*self.generateFrames())
self.baseline, self.frames, self.diffs = zip(*self.generate_frames())
assert not any(any(diffs[0].image.convert("L").point(bool).getdata()) for diffs in self.diffs)
def generateBaseline(self):
for i, frame in zip(itertools.count(), self.tracer.video(**self.outputSettings())):
def generate_baseline(self):
for i, frame in zip(itertools.count(), self.tracer.video(**self.output_settings())):
frame.save(os.path.join(self.path, self.BASELINE % i))
def gatherTests(root=os.getcwd()):
def gather_tests(root=os.getcwd()):
tests = CinemaTest([], root)
for path, _, files in os.walk(root):
test = [f for f in files if re.match(CinemaTest.TEST, f)]
@ -85,12 +90,12 @@ def gatherTests(root=os.getcwd()):
continue
prefix = os.path.commonprefix([path, root])
suffix = path[len(prefix)+1:]
testPath = suffix.split(os.sep)
testRoot = tests
for component in testPath[:-1]:
newTest = testRoot.tests.get(component)
if not newTest:
newTest = testRoot.addTest(component)
testRoot = newTest
testRoot.addTest(testPath[-1], VideoTest)
test_path = suffix.split(os.sep)
test_root = tests
for component in test_path[:-1]:
new_test = test_root.tests.get(component)
if not new_test:
new_test = test_root.add_test(component)
test_root = new_test
test_root.add_test(test_path[-1], VideoTest)
return tests

View File

@ -1,8 +1,8 @@
def dictMerge(a, b):
def dict_merge(a, b):
for key, value in b.items():
if isinstance(value, dict):
if key in a:
dictMerge(a[key], value)
dict_merge(a[key], value)
else:
a[key] = dict(value)
else:

View File

@ -3,31 +3,34 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
from collections import namedtuple
def createCallback(structName, cbName, funcName=None):
funcName = funcName or "_py{}{}".format(structName, cbName[0].upper() + cbName[1:])
fullStruct = "struct {}*".format(structName)
def cb(handle, *args):
h = ffi.cast(fullStruct, handle)
return getattr(ffi.from_handle(h.pyobj), cbName)(*args)
return ffi.def_extern(name=funcName)(cb)
def create_callback(struct_name, cb_name, func_name=None):
func_name = func_name or "_py{}{}".format(struct_name, cb_name[0].upper() + cb_name[1:])
full_struct = "struct {}*".format(struct_name)
version = ffi.string(lib.projectVersion).decode('utf-8')
def callback(handle, *args):
handle = ffi.cast(full_struct, handle)
return getattr(ffi.from_handle(handle.pyobj), cb_name)(*args)
return ffi.def_extern(name=func_name)(callback)
__version__ = ffi.string(lib.projectVersion).decode('utf-8')
GitInfo = namedtuple("GitInfo", "commit commitShort branch revision")
git = {}
GIT = {}
if lib.gitCommit and lib.gitCommit != "(unknown)":
git['commit'] = ffi.string(lib.gitCommit).decode('utf-8')
GIT['commit'] = ffi.string(lib.gitCommit).decode('utf-8')
if lib.gitCommitShort and lib.gitCommitShort != "(unknown)":
git['commitShort'] = ffi.string(lib.gitCommitShort).decode('utf-8')
GIT['commitShort'] = ffi.string(lib.gitCommitShort).decode('utf-8')
if lib.gitBranch and lib.gitBranch != "(unknown)":
git['branch'] = ffi.string(lib.gitBranch).decode('utf-8')
GIT['branch'] = ffi.string(lib.gitBranch).decode('utf-8')
if lib.gitRevision > 0:
git['revision'] = lib.gitRevision
GIT['revision'] = lib.gitRevision
git = GitInfo(**git)
GIT = GitInfo(**GIT)

View File

@ -3,21 +3,23 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
class _ARMRegisters:
def __init__(self, cpu):
self._cpu = cpu
def __getitem__(self, r):
if r > lib.ARM_PC:
def __getitem__(self, reg):
if reg > lib.ARM_PC:
raise IndexError("Register out of range")
return self._cpu._native.gprs[r]
return self._cpu._native.gprs[reg]
def __setitem__(self, r, value):
if r >= lib.ARM_PC:
def __setitem__(self, reg, value):
if reg >= lib.ARM_PC:
raise IndexError("Register out of range")
self._cpu._native.gprs[r] = value
self._cpu._native.gprs[reg] = value
class ARMCore:
def __init__(self, native):
@ -25,4 +27,3 @@ class ARMCore:
self.gprs = _ARMRegisters(self)
self.cpsr = self._native.cpsr
self.spsr = self._native.spsr

View File

@ -3,9 +3,11 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from . import tile, createCallback
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
from . import tile
from cached_property import cached_property
from functools import wraps
def find(path):
core = lib.mCoreFind(path.encode('UTF-8'))
@ -13,82 +15,95 @@ def find(path):
return None
return Core._init(core)
def findVF(vf):
core = lib.mCoreFindVF(vf.handle)
def find_vf(vfile):
core = lib.mCoreFindVF(vfile.handle)
if core == ffi.NULL:
return None
return Core._init(core)
def loadPath(path):
def load_path(path):
core = find(path)
if not core or not core.loadFile(path):
if not core or not core.load_file(path):
return None
return core
def loadVF(vf):
core = findVF(vf)
if not core or not core.loadROM(vf):
def load_vf(vfile):
core = find_vf(vfile)
if not core or not core.load_rom(vfile):
return None
return core
def needsReset(f):
def needs_reset(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if not self._wasReset:
if not self._was_reset:
raise RuntimeError("Core must be reset first")
return f(self, *args, **kwargs)
return func(self, *args, **kwargs)
return wrapper
def protected(f):
def protected(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if self._protected:
raise RuntimeError("Core is protected")
return f(self, *args, **kwargs)
return func(self, *args, **kwargs)
return wrapper
@ffi.def_extern()
def _mCorePythonCallbacksVideoFrameStarted(user):
context = ffi.from_handle(user)
context._videoFrameStarted()
@ffi.def_extern()
def _mCorePythonCallbacksVideoFrameEnded(user):
def _mCorePythonCallbacksVideoFrameStarted(user): # pylint: disable=invalid-name
context = ffi.from_handle(user)
context._videoFrameEnded()
context._video_frame_started()
@ffi.def_extern()
def _mCorePythonCallbacksCoreCrashed(user):
def _mCorePythonCallbacksVideoFrameEnded(user): # pylint: disable=invalid-name
context = ffi.from_handle(user)
context._coreCrashed()
context._video_frame_ended()
@ffi.def_extern()
def _mCorePythonCallbacksSleep(user):
def _mCorePythonCallbacksCoreCrashed(user): # pylint: disable=invalid-name
context = ffi.from_handle(user)
context._core_crashed()
@ffi.def_extern()
def _mCorePythonCallbacksSleep(user): # pylint: disable=invalid-name
context = ffi.from_handle(user)
context._sleep()
class CoreCallbacks(object):
def __init__(self):
self._handle = ffi.new_handle(self)
self.videoFrameStarted = []
self.videoFrameEnded = []
self.coreCrashed = []
self.video_frame_started = []
self.video_frame_ended = []
self.core_crashed = []
self.sleep = []
self.context = lib.mCorePythonCallbackCreate(self._handle)
def _videoFrameStarted(self):
for cb in self.videoFrameStarted:
cb()
def _video_frame_started(self):
for callback in self.video_frame_started:
callback()
def _videoFrameEnded(self):
for cb in self.videoFrameEnded:
cb()
def _video_frame_ended(self):
for callback in self.video_frame_ended:
callback()
def _coreCrashed(self):
for cb in self.coreCrashed:
cb()
def _core_crashed(self):
for callback in self.core_crashed:
callback()
def _sleep(self):
for cb in self.sleep:
cb()
for callback in self.sleep:
callback()
class Core(object):
if hasattr(lib, 'PLATFORM_GBA'):
@ -99,36 +114,36 @@ class Core(object):
def __init__(self, native):
self._core = native
self._wasReset = False
self._was_reset = False
self._protected = False
self._callbacks = CoreCallbacks()
self._core.addCoreCallbacks(self._core, self._callbacks.context)
self.config = Config(ffi.addressof(native.config))
def __del__(self):
self._wasReset = False
self._was_reset = False
@cached_property
def graphicsCache(self):
if not self._wasReset:
def graphics_cache(self):
if not self._was_reset:
raise RuntimeError("Core must be reset first")
return tile.CacheSet(self)
@cached_property
def tiles(self):
t = []
ts = ffi.addressof(self.graphicsCache.cache.tiles)
for i in range(lib.mTileCacheSetSize(ts)):
t.append(tile.TileView(lib.mTileCacheSetGetPointer(ts, i)))
return t
tiles = []
native_tiles = ffi.addressof(self.graphics_cache.cache.tiles)
for i in range(lib.mTileCacheSetSize(native_tiles)):
tiles.append(tile.TileView(lib.mTileCacheSetGetPointer(native_tiles, i)))
return tiles
@cached_property
def maps(self):
m = []
ms = ffi.addressof(self.graphicsCache.cache.maps)
for i in range(lib.mMapCacheSetSize(ms)):
m.append(tile.MapView(lib.mMapCacheSetGetPointer(ms, i)))
return m
maps = []
native_maps = ffi.addressof(self.graphics_cache.cache.maps)
for i in range(lib.mMapCacheSetSize(native_maps)):
maps.append(tile.MapView(lib.mMapCacheSetGetPointer(native_maps, i)))
return maps
@classmethod
def _init(cls, native):
@ -150,73 +165,73 @@ class Core(object):
return Core(core)
def _load(self):
self._wasReset = True
self._was_reset = True
def loadFile(self, path):
def load_file(self, path):
return bool(lib.mCoreLoadFile(self._core, path.encode('UTF-8')))
def isROM(self, vf):
return bool(self._core.isROM(vf.handle))
def is_rom(self, vfile):
return bool(self._core.isROM(vfile.handle))
def loadROM(self, vf):
return bool(self._core.loadROM(self._core, vf.handle))
def load_rom(self, vfile):
return bool(self._core.loadROM(self._core, vfile.handle))
def loadBIOS(self, vf, id=0):
return bool(self._core.loadBIOS(self._core, vf.handle, id))
def load_bios(self, vfile, id=0):
return bool(self._core.loadBIOS(self._core, vfile.handle, id))
def loadSave(self, vf):
return bool(self._core.loadSave(self._core, vf.handle))
def load_save(self, vfile):
return bool(self._core.loadSave(self._core, vfile.handle))
def loadTemporarySave(self, vf):
return bool(self._core.loadTemporarySave(self._core, vf.handle))
def load_temporary_save(self, vfile):
return bool(self._core.loadTemporarySave(self._core, vfile.handle))
def loadPatch(self, vf):
return bool(self._core.loadPatch(self._core, vf.handle))
def load_patch(self, vfile):
return bool(self._core.loadPatch(self._core, vfile.handle))
def loadConfig(self, config):
def load_config(self, config):
lib.mCoreLoadForeignConfig(self._core, config._native)
def autoloadSave(self):
def autoload_save(self):
return bool(lib.mCoreAutoloadSave(self._core))
def autoloadPatch(self):
def autoload_patch(self):
return bool(lib.mCoreAutoloadPatch(self._core))
def autoloadCheats(self):
def autoload_cheats(self):
return bool(lib.mCoreAutoloadCheats(self._core))
def platform(self):
return self._core.platform(self._core)
def desiredVideoDimensions(self):
def desired_video_dimensions(self):
width = ffi.new("unsigned*")
height = ffi.new("unsigned*")
self._core.desiredVideoDimensions(self._core, width, height)
return width[0], height[0]
def setVideoBuffer(self, image):
def set_video_buffer(self, image):
self._core.setVideoBuffer(self._core, image.buffer, image.stride)
def reset(self):
self._core.reset(self._core)
self._load()
@needsReset
@needs_reset
@protected
def runFrame(self):
def run_frame(self):
self._core.runFrame(self._core)
@needsReset
@needs_reset
@protected
def runLoop(self):
def run_loop(self):
self._core.runLoop(self._core)
@needsReset
@needs_reset
def step(self):
self._core.step(self._core)
@staticmethod
def _keysToInt(*args, **kwargs):
def _keys_to_int(*args, **kwargs):
keys = 0
if 'raw' in kwargs:
keys = kwargs['raw']
@ -224,22 +239,25 @@ class Core(object):
keys |= 1 << key
return keys
def setKeys(self, *args, **kwargs):
self._core.setKeys(self._core, self._keysToInt(*args, **kwargs))
@protected
def set_keys(self, *args, **kwargs):
self._core.setKeys(self._core, self._keys_to_int(*args, **kwargs))
def addKeys(self, *args, **kwargs):
self._core.addKeys(self._core, self._keysToInt(*args, **kwargs))
@protected
def add_keys(self, *args, **kwargs):
self._core.addKeys(self._core, self._keys_to_int(*args, **kwargs))
def clearKeys(self, *args, **kwargs):
self._core.clearKeys(self._core, self._keysToInt(*args, **kwargs))
@protected
def clear_keys(self, *args, **kwargs):
self._core.clearKeys(self._core, self._keys_to_int(*args, **kwargs))
@property
@needsReset
def frameCounter(self):
@needs_reset
def frame_counter(self):
return self._core.frameCounter(self._core)
@property
def frameCycles(self):
def frame_cycles(self):
return self._core.frameCycles(self._core)
@property
@ -247,24 +265,25 @@ class Core(object):
return self._core.frequency(self._core)
@property
def gameTitle(self):
def game_title(self):
title = ffi.new("char[16]")
self._core.getGameTitle(self._core, title)
return ffi.string(title, 16).decode("ascii")
@property
def gameCode(self):
def game_code(self):
code = ffi.new("char[12]")
self._core.getGameCode(self._core, code)
return ffi.string(code, 12).decode("ascii")
def addFrameCallback(self, cb):
self._callbacks.videoFrameEnded.append(cb)
def add_frame_callback(self, callback):
self._callbacks.video_frame_ended.append(callback)
@property
def crc32(self):
return self._native.romCrc32
class ICoreOwner(object):
def claim(self):
raise NotImplementedError
@ -281,6 +300,7 @@ class ICoreOwner(object):
self.core._protected = False
self.release()
class IRunner(object):
def pause(self):
raise NotImplementedError
@ -288,15 +308,18 @@ class IRunner(object):
def unpause(self):
raise NotImplementedError
def useCore(self):
def use_core(self):
raise NotImplementedError
def isRunning(self):
@property
def running(self):
raise NotImplementedError
def isPaused(self):
@property
def paused(self):
raise NotImplementedError
class Config(object):
def __init__(self, native=None, port=None, defaults={}):
if not native:

View File

@ -3,26 +3,27 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
from .core import IRunner, ICoreOwner, Core
import io
import sys
class DebuggerCoreOwner(ICoreOwner):
def __init__(self, debugger):
self.debugger = debugger
self.wasPaused = False
self.was_paused = False
def claim(self):
if self.debugger.isRunning():
self.wasPaused = True
self.was_paused = True
self.debugger.pause()
return self.debugger._core
def release(self):
if self.wasPaused:
if self.was_paused:
self.debugger.unpause()
class NativeDebugger(IRunner):
WATCHPOINT_WRITE = lib.WATCHPOINT_WRITE
WATCHPOINT_READ = lib.WATCHPOINT_READ
@ -49,37 +50,40 @@ class NativeDebugger(IRunner):
def unpause(self):
self._native.state = lib.DEBUGGER_RUNNING
def isRunning(self):
@property
def running(self):
return self._native.state == lib.DEBUGGER_RUNNING
def isPaused(self):
@property
def paused(self):
return self._native.state in (lib.DEBUGGER_PAUSED, lib.DEBUGGER_CUSTOM)
def useCore(self):
def use_core(self):
return DebuggerCoreOwner(self)
def setBreakpoint(self, address):
def set_breakpoint(self, address):
if not self._native.platform.setBreakpoint:
raise RuntimeError("Platform does not support breakpoints")
self._native.platform.setBreakpoint(self._native.platform, address)
def clearBreakpoint(self, address):
def clear_breakpoint(self, address):
if not self._native.platform.setBreakpoint:
raise RuntimeError("Platform does not support breakpoints")
self._native.platform.clearBreakpoint(self._native.platform, address)
def setWatchpoint(self, address):
def set_watchpoint(self, address):
if not self._native.platform.setWatchpoint:
raise RuntimeError("Platform does not support watchpoints")
self._native.platform.setWatchpoint(self._native.platform, address)
def clearWatchpoint(self, address):
def clear_watchpoint(self, address):
if not self._native.platform.clearWatchpoint:
raise RuntimeError("Platform does not support watchpoints")
self._native.platform.clearWatchpoint(self._native.platform, address)
def addCallback(self, cb):
self._cbs.append(cb)
def add_callback(self, callback):
self._cbs.append(callback)
class CLIBackend(object):
def __init__(self, backend):
@ -88,6 +92,7 @@ class CLIBackend(object):
def write(self, string):
self.backend.printf(string)
class CLIDebugger(NativeDebugger):
def __init__(self, native):
super(CLIDebugger, self).__init__(native)
@ -97,5 +102,5 @@ class CLIDebugger(NativeDebugger):
message = message.format(*args, **kwargs)
self._cli.backend.printf(ffi.new("char []", b"%s"), ffi.new("char []", message.encode('utf-8')))
def installPrint(self):
def install_print(self):
sys.stdout = CLIBackend(self)

View File

@ -8,6 +8,7 @@ try:
except ImportError:
pass
def search(core):
crc32 = None
if hasattr(core, 'PLATFORM_GBA') and core.platform() == core.PLATFORM_GBA:

View File

@ -3,12 +3,13 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
from .lr35902 import LR35902Core
from .core import Core, needsReset
from .core import Core, needs_reset
from .memory import Memory
from .tile import Sprite
from . import createCallback
from . import create_callback
class GB(Core):
KEY_A = lib.GBA_KEY_A
@ -25,31 +26,34 @@ class GB(Core):
self._native = ffi.cast("struct GB*", native.board)
self.sprites = GBObjs(self)
self.cpu = LR35902Core(self._core.cpu)
self.memory = None
@needsReset
def _initCache(self, cache):
@needs_reset
def _init_cache(self, cache):
lib.GBVideoCacheInit(cache)
lib.GBVideoCacheAssociate(cache, ffi.addressof(self._native.video))
def _deinitCache(self, cache):
def _deinit_cache(self, cache):
lib.mCacheSetDeinit(cache)
if self._wasReset:
if self._was_reset:
self._native.video.renderer.cache = ffi.NULL
def _load(self):
super(GB, self)._load()
self.memory = GBMemory(self._core)
def attachSIO(self, link):
def attach_sio(self, link):
lib.GBSIOSetDriver(ffi.addressof(self._native.sio), link._native)
def __del__(self):
lib.GBSIOSetDriver(ffi.addressof(self._native.sio), ffi.NULL)
createCallback("GBSIOPythonDriver", "init")
createCallback("GBSIOPythonDriver", "deinit")
createCallback("GBSIOPythonDriver", "writeSB")
createCallback("GBSIOPythonDriver", "writeSC")
create_callback("GBSIOPythonDriver", "init")
create_callback("GBSIOPythonDriver", "deinit")
create_callback("GBSIOPythonDriver", "writeSB")
create_callback("GBSIOPythonDriver", "writeSC")
class GBSIODriver(object):
def __init__(self):
@ -62,53 +66,55 @@ class GBSIODriver(object):
def deinit(self):
pass
def writeSB(self, value):
def write_sb(self, value):
pass
def writeSC(self, value):
def write_sc(self, value):
return value
class GBSIOSimpleDriver(GBSIODriver):
def __init__(self, period=0x100):
super(GBSIOSimpleDriver, self).__init__()
self.rx = 0x00
self.rx = 0x00 # pylint: disable=invalid-name
self._period = period
def init(self):
self._native.p.period = self._period
return True
def writeSB(self, value):
self.rx = value
def write_sb(self, value):
self.rx = value # pylint: disable=invalid-name
def writeSC(self, value):
def write_sc(self, value):
self._native.p.period = self._period
if value & 0x80:
lib.mTimingDeschedule(ffi.addressof(self._native.p.p.timing), ffi.addressof(self._native.p.event))
lib.mTimingSchedule(ffi.addressof(self._native.p.p.timing), ffi.addressof(self._native.p.event), self._native.p.period)
return value
def isReady(self):
def is_ready(self):
return not self._native.p.remainingBits
@property
def tx(self):
self._native.p.pendingSB
def tx(self): # pylint: disable=invalid-name
return self._native.p.pendingSB
@property
def period(self):
return self._native.p.period
@tx.setter
def tx(self, newTx):
def tx(self, newTx): # pylint: disable=invalid-name
self._native.p.pendingSB = newTx
self._native.p.remainingBits = 8
@period.setter
def period(self, newPeriod):
self._period = newPeriod
def period(self, new_period):
self._period = new_period
if self._native.p:
self._native.p.period = newPeriod
self._native.p.period = new_period
class GBMemory(Memory):
def __init__(self, core):
@ -119,15 +125,16 @@ class GBMemory(Memory):
self.sram = Memory(core, lib.GB_SIZE_EXTERNAL_RAM, lib.GB_REGION_EXTERNAL_RAM)
self.iwram = Memory(core, lib.GB_SIZE_WORKING_RAM_BANK0, lib.GB_BASE_WORKING_RAM_BANK0)
self.oam = Memory(core, lib.GB_SIZE_OAM, lib.GB_BASE_OAM)
self.io = Memory(core, lib.GB_SIZE_IO, lib.GB_BASE_IO)
self.io = Memory(core, lib.GB_SIZE_IO, lib.GB_BASE_IO) # pylint: disable=invalid-name
self.hram = Memory(core, lib.GB_SIZE_HRAM, lib.GB_BASE_HRAM)
class GBSprite(Sprite):
PALETTE_BASE = 8,
PALETTE_BASE = (8,)
def __init__(self, obj, core):
self.x = obj.x
self.y = obj.y
self.x = obj.x # pylint: disable=invalid-name
self.y = obj.y # pylint: disable=invalid-name
self.tile = obj.tile
self._attr = obj.attr
self.width = 8
@ -136,10 +143,10 @@ class GBSprite(Sprite):
if core._native.model >= lib.GB_MODEL_CGB:
if self._attr & 8:
self.tile += 512
self.paletteId = self._attr & 7
self.palette_id = self._attr & 7
else:
self.paletteId = (self._attr >> 4) & 1
self.paletteId += 8
self.palette_id = (self._attr >> 4) & 1
self.palette_id += 8
class GBObjs:

View File

@ -3,12 +3,13 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
from .arm import ARMCore
from .core import Core, needsReset
from .core import Core, needs_reset
from .tile import Sprite
from .memory import Memory
from . import createCallback
from . import create_callback
class GBA(Core):
KEY_A = lib.GBA_KEY_A
@ -34,23 +35,24 @@ class GBA(Core):
self._native = ffi.cast("struct GBA*", native.board)
self.sprites = GBAObjs(self)
self.cpu = ARMCore(self._core.cpu)
self.memory = None
self._sio = set()
@needsReset
def _initCache(self, cache):
@needs_reset
def _init_cache(self, cache):
lib.GBAVideoCacheInit(cache)
lib.GBAVideoCacheAssociate(cache, ffi.addressof(self._native.video))
def _deinitCache(self, cache):
def _deinit_cache(self, cache):
lib.mCacheSetDeinit(cache)
if self._wasReset:
if self._was_reset:
self._native.video.renderer.cache = ffi.NULL
def _load(self):
super(GBA, self)._load()
self.memory = GBAMemory(self._core, self._native.memory.romSize)
def attachSIO(self, link, mode=lib.SIO_MULTI):
def attach_sio(self, link, mode=lib.SIO_MULTI):
self._sio.add(mode)
lib.GBASIOSetDriver(ffi.addressof(self._native.sio), link._native, mode)
@ -58,14 +60,18 @@ class GBA(Core):
for mode in self._sio:
lib.GBASIOSetDriver(ffi.addressof(self._native.sio), ffi.NULL, mode)
createCallback("GBASIOPythonDriver", "init")
createCallback("GBASIOPythonDriver", "deinit")
createCallback("GBASIOPythonDriver", "load")
createCallback("GBASIOPythonDriver", "unload")
createCallback("GBASIOPythonDriver", "writeRegister")
create_callback("GBASIOPythonDriver", "init")
create_callback("GBASIOPythonDriver", "deinit")
create_callback("GBASIOPythonDriver", "load")
create_callback("GBASIOPythonDriver", "unload")
create_callback("GBASIOPythonDriver", "writeRegister")
class GBASIODriver(object):
def __init__(self):
super(GBASIODriver, self).__init__()
self._handle = ffi.new_handle(self)
self._native = ffi.gc(lib.GBASIOPythonDriverCreate(self._handle), lib.free)
@ -81,9 +87,10 @@ class GBASIODriver(object):
def unload(self):
return True
def writeRegister(self, address, value):
def write_register(self, address, value):
return value
class GBASIOJOYDriver(GBASIODriver):
RESET = lib.JOY_RESET
POLL = lib.JOY_POLL
@ -91,10 +98,11 @@ class GBASIOJOYDriver(GBASIODriver):
RECV = lib.JOY_RECV
def __init__(self):
self._handle = ffi.new_handle(self)
super(GBASIOJOYDriver, self).__init__()
self._native = ffi.gc(lib.GBASIOJOYPythonDriverCreate(self._handle), lib.free)
def sendCommand(self, cmd, data):
def send_command(self, cmd, data):
buffer = ffi.new('uint8_t[5]')
try:
buffer[0] = data[0]
@ -110,6 +118,7 @@ class GBASIOJOYDriver(GBASIODriver):
return bytes(buffer[0:outlen])
return None
class GBAMemory(Memory):
def __init__(self, core, romSize=lib.SIZE_CART0):
super(GBAMemory, self).__init__(core, 0x100000000)
@ -117,7 +126,7 @@ class GBAMemory(Memory):
self.bios = Memory(core, lib.SIZE_BIOS, lib.BASE_BIOS)
self.wram = Memory(core, lib.SIZE_WORKING_RAM, lib.BASE_WORKING_RAM)
self.iwram = Memory(core, lib.SIZE_WORKING_IRAM, lib.BASE_WORKING_IRAM)
self.io = Memory(core, lib.SIZE_IO, lib.BASE_IO)
self.io = Memory(core, lib.SIZE_IO, lib.BASE_IO) # pylint: disable=invalid-name
self.palette = Memory(core, lib.SIZE_PALETTE_RAM, lib.BASE_PALETTE_RAM)
self.vram = Memory(core, lib.SIZE_VRAM, lib.BASE_VRAM)
self.oam = Memory(core, lib.SIZE_OAM, lib.BASE_OAM)
@ -128,6 +137,7 @@ class GBAMemory(Memory):
self.rom = self.cart0
self.sram = Memory(core, lib.SIZE_CART_SRAM, lib.BASE_CART_SRAM)
class GBASprite(Sprite):
TILE_BASE = 0x800, 0x400
PALETTE_BASE = 0x10, 1
@ -136,18 +146,19 @@ class GBASprite(Sprite):
self._a = obj.a
self._b = obj.b
self._c = obj.c
self.x = self._b & 0x1FF
self.y = self._a & 0xFF
self.x = self._b & 0x1FF # pylint: disable=invalid-name
self.y = self._a & 0xFF # pylint: disable=invalid-name
self._shape = self._a >> 14
self._size = self._b >> 14
self._256Color = bool(self._a & 0x2000)
self._256_color = bool(self._a & 0x2000)
self.width, self.height = lib.GBAVideoObjSizes[self._shape * 4 + self._size]
self.tile = self._c & 0x3FF
if self._256Color:
self.paletteId = 0
if self._256_color:
self.palette_id = 0
self.tile >>= 1
else:
self.paletteId = self._c >> 12
self.palette_id = self._c >> 12
class GBAObjs:
def __init__(self, core):
@ -161,7 +172,7 @@ class GBAObjs:
if index >= len(self):
raise IndexError()
sprite = GBASprite(self._obj[index])
tiles = self._core.tiles[3 if sprite._256Color else 2]
map1D = bool(self._core._native.memory.io[0] & 0x40)
sprite.constitute(tiles, 0 if map1D else 0x20)
tiles = self._core.tiles[3 if sprite._256_color else 2]
map_1d = bool(self._core._native.memory.io[0] & 0x40)
sprite.constitute(tiles, 0 if map_1d else 0x20)
return sprite

View File

@ -3,7 +3,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi # pylint: disable=no-name-in-module
from . import png
try:
@ -11,6 +11,7 @@ try:
except ImportError:
pass
class Image:
def __init__(self, width, height, stride=0, alpha=False):
self.width = width
@ -24,58 +25,63 @@ class Image:
self.stride = self.width
self.buffer = ffi.new("color_t[{}]".format(self.stride * self.height))
def savePNG(self, f):
p = png.PNG(f, mode=png.MODE_RGBA if self.alpha else png.MODE_RGB)
success = p.writeHeader(self)
success = success and p.writePixels(self)
p.writeClose()
def save_png(self, fileobj):
png_file = png.PNG(fileobj, mode=png.MODE_RGBA if self.alpha else png.MODE_RGB)
success = png_file.write_header(self)
success = success and png_file.write_pixels(self)
png_file.write_close()
return success
if 'PImage' in globals():
def toPIL(self):
type = "RGBA" if self.alpha else "RGBX"
return PImage.frombytes(type, (self.width, self.height), ffi.buffer(self.buffer), "raw",
type, self.stride * 4)
def to_pil(self):
colorspace = "RGBA" if self.alpha else "RGBX"
return PImage.frombytes(colorspace, (self.width, self.height), ffi.buffer(self.buffer), "raw",
colorspace, self.stride * 4)
def u16ToU32(c):
r = c & 0x1F
g = (c >> 5) & 0x1F
b = (c >> 10) & 0x1F
a = (c >> 15) & 1
def u16_to_u32(color):
# pylint: disable=invalid-name
r = color & 0x1F
g = (color >> 5) & 0x1F
b = (color >> 10) & 0x1F
a = (color >> 15) & 1
abgr = r << 3
abgr |= g << 11
abgr |= b << 19
abgr |= (a * 0xFF) << 24
return abgr
def u32ToU16(c):
r = (c >> 3) & 0x1F
g = (c >> 11) & 0x1F
b = (c >> 19) & 0x1F
a = c >> 31
def u32_to_u16(color):
# pylint: disable=invalid-name
r = (color >> 3) & 0x1F
g = (color >> 11) & 0x1F
b = (color >> 19) & 0x1F
a = color >> 31
abgr = r
abgr |= g << 5
abgr |= b << 10
abgr |= a << 15
return abgr
if ffi.sizeof("color_t") == 2:
def colorToU16(c):
return c
def color_to_u16(color):
return color
colorToU32 = u16ToU32
color_to_u32 = u16_to_u32 # pylint: disable=invalid-name
def u16ToColor(c):
return c
def u16_to_color(color):
return color
u32ToColor = u32ToU16
u32_to_color = u32_to_u16 # pylint: disable=invalid-name
else:
def colorToU32(c):
return c
def color_to_u32(color):
return color
colorToU16 = u32ToU16
color_to_u16 = u32_to_u16 # pylint: disable=invalid-name
def u32ToColor(c):
return c
def u32_to_color(color):
return color
u16ToColor = u16ToU32
u16_to_color = u16_to_u32 # pylint: disable=invalid-name

View File

@ -3,17 +3,15 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from . import createCallback
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
from . import create_callback
createCallback("mLoggerPy", "log", "_pyLog")
create_callback("mLoggerPy", "log", "_pyLog")
defaultLogger = None
def installDefault(logger):
global defaultLogger
defaultLogger = logger
lib.mLogSetDefaultLogger(logger._native)
def install_default(logger):
Logger.install_default(logger)
class Logger(object):
FATAL = lib.mLOG_FATAL
@ -24,16 +22,24 @@ class Logger(object):
STUB = lib.mLOG_STUB
GAME_ERROR = lib.mLOG_GAME_ERROR
_DEFAULT_LOGGER = None
def __init__(self):
self._handle = ffi.new_handle(self)
self._native = ffi.gc(lib.mLoggerPythonCreate(self._handle), lib.free)
@staticmethod
def categoryName(category):
def category_name(category):
return ffi.string(lib.mLogCategoryName(category)).decode('UTF-8')
@classmethod
def install_default(cls, logger):
cls._DEFAULT_LOGGER = logger
lib.mLogSetDefaultLogger(logger._native)
def log(self, category, level, message):
print("{}: {}".format(self.categoryName(category), message))
print("{}: {}".format(self.category_name(category), message))
class NullLogger(Logger):
def log(self, category, level, message):

View File

@ -3,9 +3,11 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi # pylint: disable=no-name-in-module
class LR35902Core:
# pylint: disable=invalid-name
def __init__(self, native):
self._native = ffi.cast("struct LR35902Core*", native)

View File

@ -3,7 +3,8 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
class MemoryView(object):
def __init__(self, core, width, size, base=0, sign="u"):
@ -11,10 +12,10 @@ class MemoryView(object):
self._width = width
self._size = size
self._base = base
self._busRead = getattr(self._core, "busRead" + str(width * 8))
self._busWrite = getattr(self._core, "busWrite" + str(width * 8))
self._rawRead = getattr(self._core, "rawRead" + str(width * 8))
self._rawWrite = getattr(self._core, "rawWrite" + str(width * 8))
self._bus_read = getattr(self._core, "busRead" + str(width * 8))
self._bus_write = getattr(self._core, "busWrite" + str(width * 8))
self._raw_read = getattr(self._core, "rawRead" + str(width * 8))
self._raw_write = getattr(self._core, "rawWrite" + str(width * 8))
self._mask = (1 << (width * 8)) - 1 # Used to force values to fit within range so that negative values work
if sign == "u" or sign == "unsigned":
self._type = "uint{}_t".format(width * 8)
@ -23,7 +24,7 @@ class MemoryView(object):
else:
raise ValueError("Invalid sign type: '{}'".format(sign))
def _addrCheck(self, address):
def _addr_check(self, address):
if isinstance(address, slice):
start = address.start or 0
stop = self._size - self._width if address.stop is None else address.stop
@ -39,33 +40,32 @@ class MemoryView(object):
return self._size
def __getitem__(self, address):
self._addrCheck(address)
self._addr_check(address)
if isinstance(address, slice):
start = address.start or 0
stop = self._size - self._width if address.stop is None else address.stop
step = address.step or self._width
return [int(ffi.cast(self._type, self._busRead(self._core, self._base + a))) for a in range(start, stop, step)]
else:
return int(ffi.cast(self._type, self._busRead(self._core, self._base + address)))
return [int(ffi.cast(self._type, self._bus_read(self._core, self._base + a))) for a in range(start, stop, step)]
return int(ffi.cast(self._type, self._bus_read(self._core, self._base + address)))
def __setitem__(self, address, value):
self._addrCheck(address)
self._addr_check(address)
if isinstance(address, slice):
start = address.start or 0
stop = self._size - self._width if address.stop is None else address.stop
step = address.step or self._width
for a in range(start, stop, step):
self._busWrite(self._core, self._base + a, value[a] & self._mask)
for addr in range(start, stop, step):
self._bus_write(self._core, self._base + addr, value[addr] & self._mask)
else:
self._busWrite(self._core, self._base + address, value & self._mask)
self._bus_write(self._core, self._base + address, value & self._mask)
def rawRead(self, address, segment=-1):
self._addrCheck(address)
return int(ffi.cast(self._type, self._rawRead(self._core, self._base + address, segment)))
def raw_read(self, address, segment=-1):
self._addr_check(address)
return int(ffi.cast(self._type, self._raw_read(self._core, self._base + address, segment)))
def rawWrite(self, address, value, segment=-1):
self._addrCheck(address)
self._rawWrite(self._core, self._base + address, segment, value & self._mask)
def raw_write(self, address, value, segment=-1):
self._addr_check(address)
self._raw_write(self._core, self._base + address, segment, value & self._mask)
class MemorySearchResult(object):
@ -75,11 +75,12 @@ class MemorySearchResult(object):
self.guessDivisor = result.guessDivisor
self.type = result.type
if result.type == Memory.SEARCH_8:
if result.type == Memory.SEARCH_INT:
if result.width == 1:
self._memory = memory.u8
elif result.type == Memory.SEARCH_16:
elif result.width == 2:
self._memory = memory.u16
elif result.type == Memory.SEARCH_32:
elif result.width == 4:
self._memory = memory.u32
elif result.type == Memory.SEARCH_STRING:
self._memory = memory.u8
@ -123,7 +124,7 @@ class Memory(object):
self.s32 = MemoryView(core, 4, size, base, "s")
def __len__(self):
return self._size
return self.size
def search(self, value, type=SEARCH_GUESS, flags=RW, limit=10000, old_results=[]):
results = ffi.new("struct mCoreMemorySearchResults*")
@ -138,11 +139,11 @@ class Memory(object):
params.valueStr = ffi.new("char[]", str(value).encode("ascii"))
for result in old_results:
r = lib.mCoreMemorySearchResultsAppend(results)
r.address = result.address
r.segment = result.segment
r.guessDivisor = result.guessDivisor
r.type = result.type
native_result = lib.mCoreMemorySearchResultsAppend(results)
native_result.address = result.address
native_result.segment = result.segment
native_result.guessDivisor = result.guessDivisor
native_result.type = result.type
if old_results:
lib.mCoreMemorySearchRepeat(self._core, params, results)
else:
@ -154,5 +155,4 @@ class Memory(object):
def __getitem__(self, address):
if isinstance(address, slice):
return bytearray(self.u8[address])
else:
return self.u8[address]

View File

@ -3,20 +3,23 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
from . import vfs
MODE_RGB = 0
MODE_RGBA = 1
MODE_INDEX = 2
class PNG:
def __init__(self, f, mode=MODE_RGB):
self.vf = vfs.open(f)
self._vfile = vfs.open(f)
self._png = None
self._info = None
self.mode = mode
def writeHeader(self, image):
self._png = lib.PNGWriteOpen(self.vf.handle)
def write_header(self, image):
self._png = lib.PNGWriteOpen(self._vfile.handle)
if self.mode == MODE_RGB:
self._info = lib.PNGWriteHeader(self._png, image.width, image.height)
if self.mode == MODE_RGBA:
@ -25,15 +28,16 @@ class PNG:
self._info = lib.PNGWriteHeader8(self._png, image.width, image.height)
return self._info != ffi.NULL
def writePixels(self, image):
def write_pixels(self, image):
if self.mode == MODE_RGB:
return lib.PNGWritePixels(self._png, image.width, image.height, image.stride, image.buffer)
if self.mode == MODE_RGBA:
return lib.PNGWritePixelsA(self._png, image.width, image.height, image.stride, image.buffer)
if self.mode == MODE_INDEX:
return lib.PNGWritePixels8(self._png, image.width, image.height, image.stride, image.buffer)
return False
def writeClose(self):
def write_close(self):
lib.PNGWriteClose(self._png, self._info)
del self._png
del self._info
self._png = None
self._info = None

View File

@ -3,9 +3,10 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
from .core import IRunner, ICoreOwner, Core
class ThreadCoreOwner(ICoreOwner):
def __init__(self, thread):
self.thread = thread
@ -19,12 +20,13 @@ class ThreadCoreOwner(ICoreOwner):
def release(self):
lib.mCoreThreadContinue(self.thread._native)
class Thread(IRunner):
def __init__(self, native=None):
if native:
self._native = native
self._core = Core(native.core)
self._core._wasReset = lib.mCoreThreadHasStarted(self._native)
self._core._was_reset = lib.mCoreThreadHasStarted(self._native)
else:
self._native = ffi.new("struct mCoreThread*")
@ -34,7 +36,7 @@ class Thread(IRunner):
self._core = core
self._native.core = core._core
lib.mCoreThreadStart(self._native)
self._core._wasReset = lib.mCoreThreadHasStarted(self._native)
self._core._was_reset = lib.mCoreThreadHasStarted(self._native)
def end(self):
if not lib.mCoreThreadHasStarted(self._native):
@ -48,11 +50,13 @@ class Thread(IRunner):
def unpause(self):
lib.mCoreThreadUnpause(self._native)
def isRunning(self):
@property
def running(self):
return bool(lib.mCoreThreadIsActive(self._native))
def isPaused(self):
@property
def paused(self):
return bool(lib.mCoreThreadIsPaused(self._native))
def useCore(self):
def use_core(self):
return ThreadCoreOwner(self)

View File

@ -3,14 +3,15 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
from . import image
class Tile:
def __init__(self, data):
self.buffer = data
def toImage(self):
def to_image(self):
i = image.Image(8, 8)
self.composite(i, 0, 0)
return i
@ -19,19 +20,22 @@ class Tile:
for iy in range(8):
ffi.memmove(ffi.addressof(i.buffer, x + (iy + y) * i.stride), ffi.addressof(self.buffer, iy * 8), 8 * ffi.sizeof("color_t"))
class CacheSet:
def __init__(self, core):
self.core = core
self.cache = ffi.gc(ffi.new("struct mCacheSet*"), core._deinitCache)
core._initCache(self.cache)
self.cache = ffi.gc(ffi.new("struct mCacheSet*"), core._deinit_cache)
core._init_cache(self.cache)
class TileView:
def __init__(self, cache):
self.cache = cache
def getTile(self, tile, palette):
def get_tile(self, tile, palette):
return Tile(lib.mTileCacheGetTile(self.cache, tile, palette))
class MapView:
def __init__(self, cache):
self.cache = cache
@ -54,6 +58,7 @@ class MapView:
ffi.memmove(ffi.addressof(i.buffer, i.stride * y), row, self.width * 8 * ffi.sizeof("color_t"))
return i
class Sprite(object):
def constitute(self, tileView, tilePitch):
i = image.Image(self.width, self.height, alpha=True)

View File

@ -3,16 +3,18 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib
import mmap
# pylint: disable=invalid-name,unused-argument
from ._pylib import ffi, lib # pylint: disable=no-name-in-module
import os
@ffi.def_extern()
def _vfpClose(vf):
vfp = ffi.cast("struct VFilePy*", vf)
ffi.from_handle(vfp.fileobj).close()
return True
@ffi.def_extern()
def _vfpSeek(vf, offset, whence):
vfp = ffi.cast("struct VFilePy*", vf)
@ -20,6 +22,7 @@ def _vfpSeek(vf, offset, whence):
f.seek(offset, whence)
return f.tell()
@ffi.def_extern()
def _vfpRead(vf, buffer, size):
vfp = ffi.cast("struct VFilePy*", vf)
@ -27,6 +30,7 @@ def _vfpRead(vf, buffer, size):
ffi.from_handle(vfp.fileobj).readinto(pybuf)
return size
@ffi.def_extern()
def _vfpWrite(vf, buffer, size):
vfp = ffi.cast("struct VFilePy*", vf)
@ -34,19 +38,23 @@ def _vfpWrite(vf, buffer, size):
ffi.from_handle(vfp.fileobj).write(pybuf)
return size
@ffi.def_extern()
def _vfpMap(vf, size, flags):
pass
@ffi.def_extern()
def _vfpUnmap(vf, memory, size):
pass
@ffi.def_extern()
def _vfpTruncate(vf, size):
vfp = ffi.cast("struct VFilePy*", vf)
ffi.from_handle(vfp.fileobj).truncate(size)
@ffi.def_extern()
def _vfpSize(vf):
vfp = ffi.cast("struct VFilePy*", vf)
@ -57,6 +65,7 @@ def _vfpSize(vf):
f.seek(pos, os.SEEK_SET)
return size
@ffi.def_extern()
def _vfpSync(vf, buffer, size):
vfp = ffi.cast("struct VFilePy*", vf)
@ -70,15 +79,14 @@ def _vfpSync(vf, buffer, size):
os.fsync()
return True
def open(f):
def open(f): # pylint: disable=redefined-builtin
handle = ffi.new_handle(f)
vf = VFile(lib.VFileFromPython(handle))
# Prevent garbage collection
vf._fileobj = f
vf._handle = handle
vf = VFile(lib.VFileFromPython(handle), _no_gc=(f, handle))
return vf
def openPath(path, mode="r"):
def open_path(path, mode="r"):
flags = 0
if mode.startswith("r"):
flags |= os.O_RDONLY
@ -94,14 +102,19 @@ def openPath(path, mode="r"):
if "x" in mode[1:]:
flags |= os.O_EXCL
vf = lib.VFileOpen(path.encode("UTF-8"), flags);
vf = lib.VFileOpen(path.encode("UTF-8"), flags)
if vf == ffi.NULL:
return None
return VFile(vf)
class VFile:
def __init__(self, vf):
def __init__(self, vf, _no_gc=None):
self.handle = vf
self._no_gc = _no_gc
def __del__(self):
self.close()
def close(self):
return bool(self.handle.close(self.handle))
@ -112,7 +125,7 @@ class VFile:
def read(self, buffer, size):
return self.handle.read(self.handle, buffer, size)
def readAll(self, size=0):
def read_all(self, size=0):
if not size:
size = self.size()
buffer = ffi.new("char[%i]" % size)

View File

@ -1,2 +1,6 @@
[aliases]
test=pytest
[pycodestyle]
exclude = .eggs
ignore = E501,E741,E743

View File

@ -0,0 +1,38 @@
from setuptools import setup
import re
import os
import os.path
import sys
import subprocess
def get_version_component(piece):
return subprocess.check_output(['cmake', '-DPRINT_STRING={}'.format(piece), '-P', '../../../version.cmake']).decode('utf-8').strip()
version = '{}.{}.{}'.format(*(get_version_component(p) for p in ('LIB_VERSION_MAJOR', 'LIB_VERSION_MINOR', 'LIB_VERSION_PATCH')))
if not get_version_component('GIT_TAG'):
version += '.{}+g{}'.format(*(get_version_component(p) for p in ('GIT_REV', 'GIT_COMMIT_SHORT')))
setup(
name="mgba",
version=version,
author="Jeffrey Pfau",
author_email="jeffrey@endrift.com",
url="http://github.com/mgba-emu/mgba/",
packages=["mgba"],
setup_requires=['cffi>=1.6', 'pytest-runner'],
install_requires=['cffi>=1.6', 'cached-property'],
extras_require={'pil': ['Pillow>=2.3'], 'cinema': ['pyyaml', 'pytest']},
tests_require=['pytest'],
cffi_modules=["_builder.py:ffi"],
license="MPL 2.0",
classifiers=[
"Programming Language :: C",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Topic :: Games/Entertainment",
"Topic :: System :: Emulators"
]
)

View File

@ -4,7 +4,7 @@ import mgba.log
import os.path
import yaml
mgba.log.installDefault(mgba.log.NullLogger())
mgba.log.install_default(mgba.log.NullLogger())
def flatten(d):
l = []
@ -18,7 +18,7 @@ def flatten(d):
def pytest_generate_tests(metafunc):
if 'vtest' in metafunc.fixturenames:
tests = cinema.test.gatherTests(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'cinema'))
tests = cinema.test.gather_tests(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'cinema'))
testList = flatten(tests)
params = []
for test in testList:
@ -34,9 +34,9 @@ def vtest(request):
return request.param
def test_video(vtest, pytestconfig):
vtest.setUp()
vtest.setup()
if pytestconfig.getoption('--rebaseline'):
vtest.generateBaseline()
vtest.generate_baseline()
else:
try:
vtest.test()

View File

@ -4,26 +4,30 @@ import os
import mgba.vfs as vfs
from mgba._pylib import ffi
def test_vfs_open():
with open(__file__) as f:
vf = vfs.open(f)
assert vf
assert vf.close()
def test_vfs_openPath():
vf = vfs.openPath(__file__)
def test_vfs_open_path():
vf = vfs.open_path(__file__)
assert vf
assert vf.close()
def test_vfs_read():
vf = vfs.openPath(__file__)
vf = vfs.open_path(__file__)
buffer = ffi.new('char[13]')
assert vf.read(buffer, 13) == 13
assert ffi.string(buffer) == b'import pytest'
vf.close()
def test_vfs_readline():
vf = vfs.openPath(__file__)
vf = vfs.open_path(__file__)
buffer = ffi.new('char[16]')
linelen = vf.readline(buffer, 16)
assert linelen in (14, 15)
@ -33,16 +37,18 @@ def test_vfs_readline():
assert ffi.string(buffer) == b'import pytest\r\n'
vf.close()
def test_vfs_readAllSize():
vf = vfs.openPath(__file__)
buffer = vf.readAll()
def test_vfs_read_all_size():
vf = vfs.open_path(__file__)
buffer = vf.read_all()
assert buffer
assert len(buffer)
assert len(buffer) == vf.size()
vf.close()
def test_vfs_seek():
vf = vfs.openPath(__file__)
vf = vfs.open_path(__file__)
assert vf.seek(0, os.SEEK_SET) == 0
assert vf.seek(1, os.SEEK_SET) == 1
assert vf.seek(1, os.SEEK_CUR) == 2
@ -52,6 +58,7 @@ def test_vfs_seek():
assert vf.seek(-1, os.SEEK_END) == vf.size() -1
vf.close()
def test_vfs_openPath_invalid():
vf = vfs.openPath('.invalid')
def test_vfs_open_path_invalid():
vf = vfs.open_path('.invalid')
assert not vf

View File

@ -47,7 +47,7 @@ if(NOT GIT_BRANCH)
endif()
if(DEFINED PRINT_STRING)
message("${${PRINT_STRING}}")
execute_process(COMMAND ${CMAKE_COMMAND} -E echo "${${PRINT_STRING}}")
elseif(NOT VERSION_STRING_CACHE OR NOT VERSION_STRING STREQUAL VERSION_STRING_CACHE)
set(VERSION_STRING_CACHE ${VERSION_STRING} CACHE STRING "" FORCE)