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) 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 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 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 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}") set_target_properties(${BINARY_NAME}-pylib PROPERTIES COMPILE_DEFINITIONS "${OS_DEFINES};${FEATURE_DEFINES};${FUNCTION_DEFINES}")
add_custom_target(${BINARY_NAME}-py ALL 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}/.. LIBDIR=${CMAKE_CURRENT_BINARY_DIR}/.. CPPFLAGS="${INCLUDE_FLAGS}" ${PYTHON_EXECUTABLE} setup.py build -b ${CMAKE_CURRENT_BINARY_DIR}
COMMAND BINDIR=${CMAKE_CURRENT_BINARY_DIR}/.. CPPFLAGS="${INCLUDE_FLAGS}" ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/setup.py build -b ${CMAKE_CURRENT_BINARY_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${BINARY_NAME} DEPENDS ${BINARY_NAME}
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/setup.py DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/setup.py
DEPENDS ${PYTHON_HEADERS} DEPENDS ${PYTHON_HEADERS}
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/_builder.py DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/_builder.py
DEPENDS ${BINARY_NAME}-pylib) 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 BASE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_*.py)
file(GLOB SUBTESTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/*/test_*.py) file(GLOB SUBTESTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/*/test_*.py)
foreach(TEST IN LISTS BASE_TESTS SUBTESTS) foreach(TEST IN LISTS BASE_TESTS SUBTESTS)
@ -56,6 +61,8 @@ foreach(TEST IN LISTS BASE_TESTS SUBTESTS)
endif() endif()
string(REGEX REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/(tests/.*/)?test_" "" TEST_NAME "${TEST}") string(REGEX REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/(tests/.*/)?test_" "" TEST_NAME "${TEST}")
string(REPLACE ".py" "" TEST_NAME "${TEST_NAME}") 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}) add_test(NAME python-${TEST_NAME}
set_tests_properties(python-${TEST_NAME} PROPERTIES ENVIRONMENT "${PATH}=${CMAKE_CURRENT_BINARY_DIR}/..") 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() endforeach()

View File

@ -78,10 +78,6 @@ ffi.embedding_api('\n'.join(lines))
ffi.embedding_init_code(""" ffi.embedding_init_code("""
import os, os.path 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 from mgba._pylib import ffi, lib
symbols = {} symbols = {}
globalSyms = { globalSyms = {
@ -109,7 +105,7 @@ ffi.embedding_init_code("""
from mgba.vfs import VFile from mgba.vfs import VFile
vf = VFile(vf) vf = VFile(vf)
name = ffi.string(name) name = ffi.string(name)
source = vf.readAll().decode('utf-8') source = vf.read_all().decode('utf-8')
try: try:
code = compile(source, name, 'exec') code = compile(source, name, 'exec')
pendingCode.append(code) pendingCode.append(code)

View File

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

View File

@ -4,44 +4,38 @@ from . import VideoFrame
Output = namedtuple('Output', ['video']) Output = namedtuple('Output', ['video'])
class Tracer(object): class Tracer(object):
def __init__(self, core): def __init__(self, core):
self.core = core self.core = core
self.fb = Image(*core.desiredVideoDimensions()) self.framebuffer = Image(*core.desired_video_dimensions())
self.core.setVideoBuffer(self.fb) self.core.set_video_buffer(self.framebuffer)
self._videoFifo = [] self._video_fifo = []
def yieldFrames(self, skip=0, limit=None): def yield_frames(self, skip=0, limit=None):
self.core.reset() self.core.reset()
skip = (skip or 0) + 1 skip = (skip or 0) + 1
while skip > 0: while skip > 0:
frame = self.core.frameCounter frame = self.core.frame_counter
self.core.runFrame() self.core.run_frame()
skip -= 1 skip -= 1
while frame <= self.core.frameCounter and limit != 0: while frame <= self.core.frame_counter and limit != 0:
self._videoFifo.append(VideoFrame(self.fb.toPIL())) self._video_fifo.append(VideoFrame(self.framebuffer.to_pil()))
yield frame yield frame
frame = self.core.frameCounter frame = self.core.frame_counter
self.core.runFrame() self.core.run_frame()
if limit is not None: if limit is not None:
assert limit >= 0 assert limit >= 0
limit -= 1 limit -= 1
def video(self, generator=None, **kwargs): def video(self, generator=None, **kwargs):
if not generator: if not generator:
generator = self.yieldFrames(**kwargs) generator = self.yield_frames(**kwargs)
try: try:
while True: while True:
if self._videoFifo: if self._video_fifo:
result = self._videoFifo[0] yield self._video_fifo.pop(0)
self._videoFifo = self._videoFifo[1:]
yield result
else: else:
next(generator) next(generator)
except StopIteration: except StopIteration:
return 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 os
import mgba.core, mgba.image import os.path
import mgba.core
import mgba.image
import cinema.movie import cinema.movie
import itertools import itertools
import glob import glob
@ -7,20 +9,21 @@ import re
import yaml import yaml
from copy import deepcopy from copy import deepcopy
from cinema import VideoFrame from cinema import VideoFrame
from cinema.util import dictMerge from cinema.util import dict_merge
class CinemaTest(object): class CinemaTest(object):
TEST = 'test.(mvl|gb|gba|nds)' TEST = 'test.(mvl|gb|gba|nds)'
def __init__(self, path, root, settings={}): def __init__(self, path, root, settings={}):
self.fullPath = path or [] self.full_path = path or []
self.path = os.path.abspath(os.path.join(root, *self.fullPath)) self.path = os.path.abspath(os.path.join(root, *self.full_path))
self.root = root self.root = root
self.name = '.'.join(path) self.name = '.'.join(path)
self.settings = settings self.settings = settings
try: try:
with open(os.path.join(self.path, 'manifest.yml'), 'r') as f: 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: except IOError:
pass pass
self.tests = {} self.tests = {}
@ -28,41 +31,42 @@ class CinemaTest(object):
def __repr__(self): def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, self.name) 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)] 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: if 'config' in self.settings:
self.config = mgba.core.Config(defaults=self.settings['config']) self.config = mgba.core.Config(defaults=self.settings['config'])
self.core.loadConfig(self.config) self.core.load_config(self.config)
self.core.reset() self.core.reset()
def addTest(self, name, cls=None, settings={}): def add_test(self, name, cls=None, settings={}):
cls = cls or self.__class__ cls = cls or self.__class__
newSettings = deepcopy(self.settings) new_settings = deepcopy(self.settings)
dictMerge(newSettings, settings) dict_merge(new_settings, settings)
self.tests[name] = cls(self.fullPath + [name], self.root, newSettings) self.tests[name] = cls(self.full_path + [name], self.root, new_settings)
return self.tests[name] return self.tests[name]
def outputSettings(self): def output_settings(self):
outputSettings = {} output_settings = {}
if 'frames' in self.settings: if 'frames' in self.settings:
outputSettings['limit'] = self.settings['frames'] output_settings['limit'] = self.settings['frames']
if 'skip' in self.settings: if 'skip' in self.settings:
outputSettings['skip'] = self.settings['skip'] output_settings['skip'] = self.settings['skip']
return outputSettings return output_settings
def __lt__(self, other): def __lt__(self, other):
return self.path < other.path return self.path < other.path
class VideoTest(CinemaTest): class VideoTest(CinemaTest):
BASELINE = 'baseline_%04u.png' BASELINE = 'baseline_%04u.png'
def setUp(self): def setup(self):
super(VideoTest, self).setUp(); super(VideoTest, self).setup()
self.tracer = cinema.movie.Tracer(self.core) self.tracer = cinema.movie.Tracer(self.core)
def generateFrames(self): def generate_frames(self):
for i, frame in zip(itertools.count(), self.tracer.video(**self.outputSettings())): for i, frame in zip(itertools.count(), self.tracer.video(**self.output_settings())):
try: try:
baseline = VideoFrame.load(os.path.join(self.path, self.BASELINE % i)) baseline = VideoFrame.load(os.path.join(self.path, self.BASELINE % i))
yield baseline, frame, VideoFrame.diff(baseline, frame) yield baseline, frame, VideoFrame.diff(baseline, frame)
@ -70,14 +74,15 @@ class VideoTest(CinemaTest):
yield None, frame, (None, None) yield None, frame, (None, None)
def test(self): 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) assert not any(any(diffs[0].image.convert("L").point(bool).getdata()) for diffs in self.diffs)
def generateBaseline(self): def generate_baseline(self):
for i, frame in zip(itertools.count(), self.tracer.video(**self.outputSettings())): for i, frame in zip(itertools.count(), self.tracer.video(**self.output_settings())):
frame.save(os.path.join(self.path, self.BASELINE % i)) frame.save(os.path.join(self.path, self.BASELINE % i))
def gatherTests(root=os.getcwd()):
def gather_tests(root=os.getcwd()):
tests = CinemaTest([], root) tests = CinemaTest([], root)
for path, _, files in os.walk(root): for path, _, files in os.walk(root):
test = [f for f in files if re.match(CinemaTest.TEST, f)] test = [f for f in files if re.match(CinemaTest.TEST, f)]
@ -85,12 +90,12 @@ def gatherTests(root=os.getcwd()):
continue continue
prefix = os.path.commonprefix([path, root]) prefix = os.path.commonprefix([path, root])
suffix = path[len(prefix)+1:] suffix = path[len(prefix)+1:]
testPath = suffix.split(os.sep) test_path = suffix.split(os.sep)
testRoot = tests test_root = tests
for component in testPath[:-1]: for component in test_path[:-1]:
newTest = testRoot.tests.get(component) new_test = test_root.tests.get(component)
if not newTest: if not new_test:
newTest = testRoot.addTest(component) new_test = test_root.add_test(component)
testRoot = newTest test_root = new_test
testRoot.addTest(testPath[-1], VideoTest) test_root.add_test(test_path[-1], VideoTest)
return tests return tests

View File

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

View File

@ -3,31 +3,34 @@
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # 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 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") GitInfo = namedtuple("GitInfo", "commit commitShort branch revision")
git = {} GIT = {}
if lib.gitCommit and lib.gitCommit != "(unknown)": 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)": 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)": 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: 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 # 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 # 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/. # 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: class _ARMRegisters:
def __init__(self, cpu): def __init__(self, cpu):
self._cpu = cpu self._cpu = cpu
def __getitem__(self, r): def __getitem__(self, reg):
if r > lib.ARM_PC: if reg > lib.ARM_PC:
raise IndexError("Register out of range") raise IndexError("Register out of range")
return self._cpu._native.gprs[r] return self._cpu._native.gprs[reg]
def __setitem__(self, r, value): def __setitem__(self, reg, value):
if r >= lib.ARM_PC: if reg >= lib.ARM_PC:
raise IndexError("Register out of range") raise IndexError("Register out of range")
self._cpu._native.gprs[r] = value self._cpu._native.gprs[reg] = value
class ARMCore: class ARMCore:
def __init__(self, native): def __init__(self, native):
@ -25,4 +27,3 @@ class ARMCore:
self.gprs = _ARMRegisters(self) self.gprs = _ARMRegisters(self)
self.cpsr = self._native.cpsr self.cpsr = self._native.cpsr
self.spsr = self._native.spsr self.spsr = self._native.spsr

View File

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

View File

@ -3,26 +3,27 @@
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # 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 from .core import IRunner, ICoreOwner, Core
import io
import sys import sys
class DebuggerCoreOwner(ICoreOwner): class DebuggerCoreOwner(ICoreOwner):
def __init__(self, debugger): def __init__(self, debugger):
self.debugger = debugger self.debugger = debugger
self.wasPaused = False self.was_paused = False
def claim(self): def claim(self):
if self.debugger.isRunning(): if self.debugger.isRunning():
self.wasPaused = True self.was_paused = True
self.debugger.pause() self.debugger.pause()
return self.debugger._core return self.debugger._core
def release(self): def release(self):
if self.wasPaused: if self.was_paused:
self.debugger.unpause() self.debugger.unpause()
class NativeDebugger(IRunner): class NativeDebugger(IRunner):
WATCHPOINT_WRITE = lib.WATCHPOINT_WRITE WATCHPOINT_WRITE = lib.WATCHPOINT_WRITE
WATCHPOINT_READ = lib.WATCHPOINT_READ WATCHPOINT_READ = lib.WATCHPOINT_READ
@ -49,37 +50,40 @@ class NativeDebugger(IRunner):
def unpause(self): def unpause(self):
self._native.state = lib.DEBUGGER_RUNNING self._native.state = lib.DEBUGGER_RUNNING
def isRunning(self): @property
def running(self):
return self._native.state == lib.DEBUGGER_RUNNING 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) return self._native.state in (lib.DEBUGGER_PAUSED, lib.DEBUGGER_CUSTOM)
def useCore(self): def use_core(self):
return DebuggerCoreOwner(self) return DebuggerCoreOwner(self)
def setBreakpoint(self, address): def set_breakpoint(self, address):
if not self._native.platform.setBreakpoint: if not self._native.platform.setBreakpoint:
raise RuntimeError("Platform does not support breakpoints") raise RuntimeError("Platform does not support breakpoints")
self._native.platform.setBreakpoint(self._native.platform, address) self._native.platform.setBreakpoint(self._native.platform, address)
def clearBreakpoint(self, address): def clear_breakpoint(self, address):
if not self._native.platform.setBreakpoint: if not self._native.platform.setBreakpoint:
raise RuntimeError("Platform does not support breakpoints") raise RuntimeError("Platform does not support breakpoints")
self._native.platform.clearBreakpoint(self._native.platform, address) self._native.platform.clearBreakpoint(self._native.platform, address)
def setWatchpoint(self, address): def set_watchpoint(self, address):
if not self._native.platform.setWatchpoint: if not self._native.platform.setWatchpoint:
raise RuntimeError("Platform does not support watchpoints") raise RuntimeError("Platform does not support watchpoints")
self._native.platform.setWatchpoint(self._native.platform, address) self._native.platform.setWatchpoint(self._native.platform, address)
def clearWatchpoint(self, address): def clear_watchpoint(self, address):
if not self._native.platform.clearWatchpoint: if not self._native.platform.clearWatchpoint:
raise RuntimeError("Platform does not support watchpoints") raise RuntimeError("Platform does not support watchpoints")
self._native.platform.clearWatchpoint(self._native.platform, address) self._native.platform.clearWatchpoint(self._native.platform, address)
def addCallback(self, cb): def add_callback(self, callback):
self._cbs.append(cb) self._cbs.append(callback)
class CLIBackend(object): class CLIBackend(object):
def __init__(self, backend): def __init__(self, backend):
@ -88,6 +92,7 @@ class CLIBackend(object):
def write(self, string): def write(self, string):
self.backend.printf(string) self.backend.printf(string)
class CLIDebugger(NativeDebugger): class CLIDebugger(NativeDebugger):
def __init__(self, native): def __init__(self, native):
super(CLIDebugger, self).__init__(native) super(CLIDebugger, self).__init__(native)
@ -97,5 +102,5 @@ class CLIDebugger(NativeDebugger):
message = message.format(*args, **kwargs) message = message.format(*args, **kwargs)
self._cli.backend.printf(ffi.new("char []", b"%s"), ffi.new("char []", message.encode('utf-8'))) 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) sys.stdout = CLIBackend(self)

View File

@ -8,6 +8,7 @@ try:
except ImportError: except ImportError:
pass pass
def search(core): def search(core):
crc32 = None crc32 = None
if hasattr(core, 'PLATFORM_GBA') and core.platform() == core.PLATFORM_GBA: 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 # 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 # 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/. # 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 .lr35902 import LR35902Core
from .core import Core, needsReset from .core import Core, needs_reset
from .memory import Memory from .memory import Memory
from .tile import Sprite from .tile import Sprite
from . import createCallback from . import create_callback
class GB(Core): class GB(Core):
KEY_A = lib.GBA_KEY_A KEY_A = lib.GBA_KEY_A
@ -25,31 +26,34 @@ class GB(Core):
self._native = ffi.cast("struct GB*", native.board) self._native = ffi.cast("struct GB*", native.board)
self.sprites = GBObjs(self) self.sprites = GBObjs(self)
self.cpu = LR35902Core(self._core.cpu) self.cpu = LR35902Core(self._core.cpu)
self.memory = None
@needsReset @needs_reset
def _initCache(self, cache): def _init_cache(self, cache):
lib.GBVideoCacheInit(cache) lib.GBVideoCacheInit(cache)
lib.GBVideoCacheAssociate(cache, ffi.addressof(self._native.video)) lib.GBVideoCacheAssociate(cache, ffi.addressof(self._native.video))
def _deinitCache(self, cache): def _deinit_cache(self, cache):
lib.mCacheSetDeinit(cache) lib.mCacheSetDeinit(cache)
if self._wasReset: if self._was_reset:
self._native.video.renderer.cache = ffi.NULL self._native.video.renderer.cache = ffi.NULL
def _load(self): def _load(self):
super(GB, self)._load() super(GB, self)._load()
self.memory = GBMemory(self._core) self.memory = GBMemory(self._core)
def attachSIO(self, link): def attach_sio(self, link):
lib.GBSIOSetDriver(ffi.addressof(self._native.sio), link._native) lib.GBSIOSetDriver(ffi.addressof(self._native.sio), link._native)
def __del__(self): def __del__(self):
lib.GBSIOSetDriver(ffi.addressof(self._native.sio), ffi.NULL) lib.GBSIOSetDriver(ffi.addressof(self._native.sio), ffi.NULL)
createCallback("GBSIOPythonDriver", "init")
createCallback("GBSIOPythonDriver", "deinit") create_callback("GBSIOPythonDriver", "init")
createCallback("GBSIOPythonDriver", "writeSB") create_callback("GBSIOPythonDriver", "deinit")
createCallback("GBSIOPythonDriver", "writeSC") create_callback("GBSIOPythonDriver", "writeSB")
create_callback("GBSIOPythonDriver", "writeSC")
class GBSIODriver(object): class GBSIODriver(object):
def __init__(self): def __init__(self):
@ -62,53 +66,55 @@ class GBSIODriver(object):
def deinit(self): def deinit(self):
pass pass
def writeSB(self, value): def write_sb(self, value):
pass pass
def writeSC(self, value): def write_sc(self, value):
return value return value
class GBSIOSimpleDriver(GBSIODriver): class GBSIOSimpleDriver(GBSIODriver):
def __init__(self, period=0x100): def __init__(self, period=0x100):
super(GBSIOSimpleDriver, self).__init__() super(GBSIOSimpleDriver, self).__init__()
self.rx = 0x00 self.rx = 0x00 # pylint: disable=invalid-name
self._period = period self._period = period
def init(self): def init(self):
self._native.p.period = self._period self._native.p.period = self._period
return True return True
def writeSB(self, value): def write_sb(self, value):
self.rx = value self.rx = value # pylint: disable=invalid-name
def writeSC(self, value): def write_sc(self, value):
self._native.p.period = self._period self._native.p.period = self._period
if value & 0x80: if value & 0x80:
lib.mTimingDeschedule(ffi.addressof(self._native.p.p.timing), ffi.addressof(self._native.p.event)) 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) lib.mTimingSchedule(ffi.addressof(self._native.p.p.timing), ffi.addressof(self._native.p.event), self._native.p.period)
return value return value
def isReady(self): def is_ready(self):
return not self._native.p.remainingBits return not self._native.p.remainingBits
@property @property
def tx(self): def tx(self): # pylint: disable=invalid-name
self._native.p.pendingSB return self._native.p.pendingSB
@property @property
def period(self): def period(self):
return self._native.p.period return self._native.p.period
@tx.setter @tx.setter
def tx(self, newTx): def tx(self, newTx): # pylint: disable=invalid-name
self._native.p.pendingSB = newTx self._native.p.pendingSB = newTx
self._native.p.remainingBits = 8 self._native.p.remainingBits = 8
@period.setter @period.setter
def period(self, newPeriod): def period(self, new_period):
self._period = newPeriod self._period = new_period
if self._native.p: if self._native.p:
self._native.p.period = newPeriod self._native.p.period = new_period
class GBMemory(Memory): class GBMemory(Memory):
def __init__(self, core): 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.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.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.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) self.hram = Memory(core, lib.GB_SIZE_HRAM, lib.GB_BASE_HRAM)
class GBSprite(Sprite): class GBSprite(Sprite):
PALETTE_BASE = 8, PALETTE_BASE = (8,)
def __init__(self, obj, core): def __init__(self, obj, core):
self.x = obj.x self.x = obj.x # pylint: disable=invalid-name
self.y = obj.y self.y = obj.y # pylint: disable=invalid-name
self.tile = obj.tile self.tile = obj.tile
self._attr = obj.attr self._attr = obj.attr
self.width = 8 self.width = 8
@ -136,10 +143,10 @@ class GBSprite(Sprite):
if core._native.model >= lib.GB_MODEL_CGB: if core._native.model >= lib.GB_MODEL_CGB:
if self._attr & 8: if self._attr & 8:
self.tile += 512 self.tile += 512
self.paletteId = self._attr & 7 self.palette_id = self._attr & 7
else: else:
self.paletteId = (self._attr >> 4) & 1 self.palette_id = (self._attr >> 4) & 1
self.paletteId += 8 self.palette_id += 8
class GBObjs: class GBObjs:

View File

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

View File

@ -3,7 +3,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # 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 from . import png
try: try:
@ -11,6 +11,7 @@ try:
except ImportError: except ImportError:
pass pass
class Image: class Image:
def __init__(self, width, height, stride=0, alpha=False): def __init__(self, width, height, stride=0, alpha=False):
self.width = width self.width = width
@ -24,58 +25,63 @@ class Image:
self.stride = self.width self.stride = self.width
self.buffer = ffi.new("color_t[{}]".format(self.stride * self.height)) self.buffer = ffi.new("color_t[{}]".format(self.stride * self.height))
def savePNG(self, f): def save_png(self, fileobj):
p = png.PNG(f, mode=png.MODE_RGBA if self.alpha else png.MODE_RGB) png_file = png.PNG(fileobj, mode=png.MODE_RGBA if self.alpha else png.MODE_RGB)
success = p.writeHeader(self) success = png_file.write_header(self)
success = success and p.writePixels(self) success = success and png_file.write_pixels(self)
p.writeClose() png_file.write_close()
return success return success
if 'PImage' in globals(): if 'PImage' in globals():
def toPIL(self): def to_pil(self):
type = "RGBA" if self.alpha else "RGBX" colorspace = "RGBA" if self.alpha else "RGBX"
return PImage.frombytes(type, (self.width, self.height), ffi.buffer(self.buffer), "raw", return PImage.frombytes(colorspace, (self.width, self.height), ffi.buffer(self.buffer), "raw",
type, self.stride * 4) colorspace, self.stride * 4)
def u16ToU32(c):
r = c & 0x1F def u16_to_u32(color):
g = (c >> 5) & 0x1F # pylint: disable=invalid-name
b = (c >> 10) & 0x1F r = color & 0x1F
a = (c >> 15) & 1 g = (color >> 5) & 0x1F
b = (color >> 10) & 0x1F
a = (color >> 15) & 1
abgr = r << 3 abgr = r << 3
abgr |= g << 11 abgr |= g << 11
abgr |= b << 19 abgr |= b << 19
abgr |= (a * 0xFF) << 24 abgr |= (a * 0xFF) << 24
return abgr return abgr
def u32ToU16(c):
r = (c >> 3) & 0x1F def u32_to_u16(color):
g = (c >> 11) & 0x1F # pylint: disable=invalid-name
b = (c >> 19) & 0x1F r = (color >> 3) & 0x1F
a = c >> 31 g = (color >> 11) & 0x1F
b = (color >> 19) & 0x1F
a = color >> 31
abgr = r abgr = r
abgr |= g << 5 abgr |= g << 5
abgr |= b << 10 abgr |= b << 10
abgr |= a << 15 abgr |= a << 15
return abgr return abgr
if ffi.sizeof("color_t") == 2: if ffi.sizeof("color_t") == 2:
def colorToU16(c): def color_to_u16(color):
return c return color
colorToU32 = u16ToU32 color_to_u32 = u16_to_u32 # pylint: disable=invalid-name
def u16ToColor(c): def u16_to_color(color):
return c return color
u32ToColor = u32ToU16 u32_to_color = u32_to_u16 # pylint: disable=invalid-name
else: else:
def colorToU32(c): def color_to_u32(color):
return c return color
colorToU16 = u32ToU16 color_to_u16 = u32_to_u16 # pylint: disable=invalid-name
def u32ToColor(c): def u32_to_color(color):
return c 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 # 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 # 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/. # 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 createCallback from . import create_callback
createCallback("mLoggerPy", "log", "_pyLog") create_callback("mLoggerPy", "log", "_pyLog")
defaultLogger = None
def installDefault(logger): def install_default(logger):
global defaultLogger Logger.install_default(logger)
defaultLogger = logger
lib.mLogSetDefaultLogger(logger._native)
class Logger(object): class Logger(object):
FATAL = lib.mLOG_FATAL FATAL = lib.mLOG_FATAL
@ -24,16 +22,24 @@ class Logger(object):
STUB = lib.mLOG_STUB STUB = lib.mLOG_STUB
GAME_ERROR = lib.mLOG_GAME_ERROR GAME_ERROR = lib.mLOG_GAME_ERROR
_DEFAULT_LOGGER = None
def __init__(self): def __init__(self):
self._handle = ffi.new_handle(self) self._handle = ffi.new_handle(self)
self._native = ffi.gc(lib.mLoggerPythonCreate(self._handle), lib.free) self._native = ffi.gc(lib.mLoggerPythonCreate(self._handle), lib.free)
@staticmethod @staticmethod
def categoryName(category): def category_name(category):
return ffi.string(lib.mLogCategoryName(category)).decode('UTF-8') 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): def log(self, category, level, message):
print("{}: {}".format(self.categoryName(category), message)) print("{}: {}".format(self.category_name(category), message))
class NullLogger(Logger): class NullLogger(Logger):
def log(self, category, level, message): 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 # 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 # 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/. # 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: class LR35902Core:
# pylint: disable=invalid-name
def __init__(self, native): def __init__(self, native):
self._native = ffi.cast("struct LR35902Core*", 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 # 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 # 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/. # 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): class MemoryView(object):
def __init__(self, core, width, size, base=0, sign="u"): def __init__(self, core, width, size, base=0, sign="u"):
@ -11,10 +12,10 @@ class MemoryView(object):
self._width = width self._width = width
self._size = size self._size = size
self._base = base self._base = base
self._busRead = getattr(self._core, "busRead" + str(width * 8)) self._bus_read = getattr(self._core, "busRead" + str(width * 8))
self._busWrite = getattr(self._core, "busWrite" + str(width * 8)) self._bus_write = getattr(self._core, "busWrite" + str(width * 8))
self._rawRead = getattr(self._core, "rawRead" + str(width * 8)) self._raw_read = getattr(self._core, "rawRead" + str(width * 8))
self._rawWrite = getattr(self._core, "rawWrite" + 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 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": if sign == "u" or sign == "unsigned":
self._type = "uint{}_t".format(width * 8) self._type = "uint{}_t".format(width * 8)
@ -23,7 +24,7 @@ class MemoryView(object):
else: else:
raise ValueError("Invalid sign type: '{}'".format(sign)) raise ValueError("Invalid sign type: '{}'".format(sign))
def _addrCheck(self, address): def _addr_check(self, address):
if isinstance(address, slice): if isinstance(address, slice):
start = address.start or 0 start = address.start or 0
stop = self._size - self._width if address.stop is None else address.stop stop = self._size - self._width if address.stop is None else address.stop
@ -39,33 +40,32 @@ class MemoryView(object):
return self._size return self._size
def __getitem__(self, address): def __getitem__(self, address):
self._addrCheck(address) self._addr_check(address)
if isinstance(address, slice): if isinstance(address, slice):
start = address.start or 0 start = address.start or 0
stop = self._size - self._width if address.stop is None else address.stop stop = self._size - self._width if address.stop is None else address.stop
step = address.step or self._width 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)] return [int(ffi.cast(self._type, self._bus_read(self._core, self._base + a))) for a in range(start, stop, step)]
else: return int(ffi.cast(self._type, self._bus_read(self._core, self._base + address)))
return int(ffi.cast(self._type, self._busRead(self._core, self._base + address)))
def __setitem__(self, address, value): def __setitem__(self, address, value):
self._addrCheck(address) self._addr_check(address)
if isinstance(address, slice): if isinstance(address, slice):
start = address.start or 0 start = address.start or 0
stop = self._size - self._width if address.stop is None else address.stop stop = self._size - self._width if address.stop is None else address.stop
step = address.step or self._width step = address.step or self._width
for a in range(start, stop, step): for addr in range(start, stop, step):
self._busWrite(self._core, self._base + a, value[a] & self._mask) self._bus_write(self._core, self._base + addr, value[addr] & self._mask)
else: 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): def raw_read(self, address, segment=-1):
self._addrCheck(address) self._addr_check(address)
return int(ffi.cast(self._type, self._rawRead(self._core, self._base + address, segment))) return int(ffi.cast(self._type, self._raw_read(self._core, self._base + address, segment)))
def rawWrite(self, address, value, segment=-1): def raw_write(self, address, value, segment=-1):
self._addrCheck(address) self._addr_check(address)
self._rawWrite(self._core, self._base + address, segment, value & self._mask) self._raw_write(self._core, self._base + address, segment, value & self._mask)
class MemorySearchResult(object): class MemorySearchResult(object):
@ -75,11 +75,12 @@ class MemorySearchResult(object):
self.guessDivisor = result.guessDivisor self.guessDivisor = result.guessDivisor
self.type = result.type self.type = result.type
if result.type == Memory.SEARCH_8: if result.type == Memory.SEARCH_INT:
if result.width == 1:
self._memory = memory.u8 self._memory = memory.u8
elif result.type == Memory.SEARCH_16: elif result.width == 2:
self._memory = memory.u16 self._memory = memory.u16
elif result.type == Memory.SEARCH_32: elif result.width == 4:
self._memory = memory.u32 self._memory = memory.u32
elif result.type == Memory.SEARCH_STRING: elif result.type == Memory.SEARCH_STRING:
self._memory = memory.u8 self._memory = memory.u8
@ -123,7 +124,7 @@ class Memory(object):
self.s32 = MemoryView(core, 4, size, base, "s") self.s32 = MemoryView(core, 4, size, base, "s")
def __len__(self): def __len__(self):
return self._size return self.size
def search(self, value, type=SEARCH_GUESS, flags=RW, limit=10000, old_results=[]): def search(self, value, type=SEARCH_GUESS, flags=RW, limit=10000, old_results=[]):
results = ffi.new("struct mCoreMemorySearchResults*") results = ffi.new("struct mCoreMemorySearchResults*")
@ -138,11 +139,11 @@ class Memory(object):
params.valueStr = ffi.new("char[]", str(value).encode("ascii")) params.valueStr = ffi.new("char[]", str(value).encode("ascii"))
for result in old_results: for result in old_results:
r = lib.mCoreMemorySearchResultsAppend(results) native_result = lib.mCoreMemorySearchResultsAppend(results)
r.address = result.address native_result.address = result.address
r.segment = result.segment native_result.segment = result.segment
r.guessDivisor = result.guessDivisor native_result.guessDivisor = result.guessDivisor
r.type = result.type native_result.type = result.type
if old_results: if old_results:
lib.mCoreMemorySearchRepeat(self._core, params, results) lib.mCoreMemorySearchRepeat(self._core, params, results)
else: else:
@ -154,5 +155,4 @@ class Memory(object):
def __getitem__(self, address): def __getitem__(self, address):
if isinstance(address, slice): if isinstance(address, slice):
return bytearray(self.u8[address]) return bytearray(self.u8[address])
else:
return self.u8[address] return self.u8[address]

View File

@ -3,20 +3,23 @@
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # 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 from . import vfs
MODE_RGB = 0 MODE_RGB = 0
MODE_RGBA = 1 MODE_RGBA = 1
MODE_INDEX = 2 MODE_INDEX = 2
class PNG: class PNG:
def __init__(self, f, mode=MODE_RGB): 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 self.mode = mode
def writeHeader(self, image): def write_header(self, image):
self._png = lib.PNGWriteOpen(self.vf.handle) self._png = lib.PNGWriteOpen(self._vfile.handle)
if self.mode == MODE_RGB: if self.mode == MODE_RGB:
self._info = lib.PNGWriteHeader(self._png, image.width, image.height) self._info = lib.PNGWriteHeader(self._png, image.width, image.height)
if self.mode == MODE_RGBA: if self.mode == MODE_RGBA:
@ -25,15 +28,16 @@ class PNG:
self._info = lib.PNGWriteHeader8(self._png, image.width, image.height) self._info = lib.PNGWriteHeader8(self._png, image.width, image.height)
return self._info != ffi.NULL return self._info != ffi.NULL
def writePixels(self, image): def write_pixels(self, image):
if self.mode == MODE_RGB: if self.mode == MODE_RGB:
return lib.PNGWritePixels(self._png, image.width, image.height, image.stride, image.buffer) return lib.PNGWritePixels(self._png, image.width, image.height, image.stride, image.buffer)
if self.mode == MODE_RGBA: if self.mode == MODE_RGBA:
return lib.PNGWritePixelsA(self._png, image.width, image.height, image.stride, image.buffer) return lib.PNGWritePixelsA(self._png, image.width, image.height, image.stride, image.buffer)
if self.mode == MODE_INDEX: if self.mode == MODE_INDEX:
return lib.PNGWritePixels8(self._png, image.width, image.height, image.stride, image.buffer) 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) lib.PNGWriteClose(self._png, self._info)
del self._png self._png = None
del self._info self._info = None

View File

@ -3,9 +3,10 @@
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # 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 from .core import IRunner, ICoreOwner, Core
class ThreadCoreOwner(ICoreOwner): class ThreadCoreOwner(ICoreOwner):
def __init__(self, thread): def __init__(self, thread):
self.thread = thread self.thread = thread
@ -19,12 +20,13 @@ class ThreadCoreOwner(ICoreOwner):
def release(self): def release(self):
lib.mCoreThreadContinue(self.thread._native) lib.mCoreThreadContinue(self.thread._native)
class Thread(IRunner): class Thread(IRunner):
def __init__(self, native=None): def __init__(self, native=None):
if native: if native:
self._native = native self._native = native
self._core = Core(native.core) self._core = Core(native.core)
self._core._wasReset = lib.mCoreThreadHasStarted(self._native) self._core._was_reset = lib.mCoreThreadHasStarted(self._native)
else: else:
self._native = ffi.new("struct mCoreThread*") self._native = ffi.new("struct mCoreThread*")
@ -34,7 +36,7 @@ class Thread(IRunner):
self._core = core self._core = core
self._native.core = core._core self._native.core = core._core
lib.mCoreThreadStart(self._native) lib.mCoreThreadStart(self._native)
self._core._wasReset = lib.mCoreThreadHasStarted(self._native) self._core._was_reset = lib.mCoreThreadHasStarted(self._native)
def end(self): def end(self):
if not lib.mCoreThreadHasStarted(self._native): if not lib.mCoreThreadHasStarted(self._native):
@ -48,11 +50,13 @@ class Thread(IRunner):
def unpause(self): def unpause(self):
lib.mCoreThreadUnpause(self._native) lib.mCoreThreadUnpause(self._native)
def isRunning(self): @property
def running(self):
return bool(lib.mCoreThreadIsActive(self._native)) return bool(lib.mCoreThreadIsActive(self._native))
def isPaused(self): @property
def paused(self):
return bool(lib.mCoreThreadIsPaused(self._native)) return bool(lib.mCoreThreadIsPaused(self._native))
def useCore(self): def use_core(self):
return ThreadCoreOwner(self) return ThreadCoreOwner(self)

View File

@ -3,14 +3,15 @@
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # 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 from . import image
class Tile: class Tile:
def __init__(self, data): def __init__(self, data):
self.buffer = data self.buffer = data
def toImage(self): def to_image(self):
i = image.Image(8, 8) i = image.Image(8, 8)
self.composite(i, 0, 0) self.composite(i, 0, 0)
return i return i
@ -19,19 +20,22 @@ class Tile:
for iy in range(8): 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")) ffi.memmove(ffi.addressof(i.buffer, x + (iy + y) * i.stride), ffi.addressof(self.buffer, iy * 8), 8 * ffi.sizeof("color_t"))
class CacheSet: class CacheSet:
def __init__(self, core): def __init__(self, core):
self.core = core self.core = core
self.cache = ffi.gc(ffi.new("struct mCacheSet*"), core._deinitCache) self.cache = ffi.gc(ffi.new("struct mCacheSet*"), core._deinit_cache)
core._initCache(self.cache) core._init_cache(self.cache)
class TileView: class TileView:
def __init__(self, cache): def __init__(self, cache):
self.cache = cache self.cache = cache
def getTile(self, tile, palette): def get_tile(self, tile, palette):
return Tile(lib.mTileCacheGetTile(self.cache, tile, palette)) return Tile(lib.mTileCacheGetTile(self.cache, tile, palette))
class MapView: class MapView:
def __init__(self, cache): def __init__(self, cache):
self.cache = 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")) ffi.memmove(ffi.addressof(i.buffer, i.stride * y), row, self.width * 8 * ffi.sizeof("color_t"))
return i return i
class Sprite(object): class Sprite(object):
def constitute(self, tileView, tilePitch): def constitute(self, tileView, tilePitch):
i = image.Image(self.width, self.height, alpha=True) 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 # 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 # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
from ._pylib import ffi, lib # pylint: disable=invalid-name,unused-argument
import mmap from ._pylib import ffi, lib # pylint: disable=no-name-in-module
import os import os
@ffi.def_extern() @ffi.def_extern()
def _vfpClose(vf): def _vfpClose(vf):
vfp = ffi.cast("struct VFilePy*", vf) vfp = ffi.cast("struct VFilePy*", vf)
ffi.from_handle(vfp.fileobj).close() ffi.from_handle(vfp.fileobj).close()
return True return True
@ffi.def_extern() @ffi.def_extern()
def _vfpSeek(vf, offset, whence): def _vfpSeek(vf, offset, whence):
vfp = ffi.cast("struct VFilePy*", vf) vfp = ffi.cast("struct VFilePy*", vf)
@ -20,6 +22,7 @@ def _vfpSeek(vf, offset, whence):
f.seek(offset, whence) f.seek(offset, whence)
return f.tell() return f.tell()
@ffi.def_extern() @ffi.def_extern()
def _vfpRead(vf, buffer, size): def _vfpRead(vf, buffer, size):
vfp = ffi.cast("struct VFilePy*", vf) vfp = ffi.cast("struct VFilePy*", vf)
@ -27,6 +30,7 @@ def _vfpRead(vf, buffer, size):
ffi.from_handle(vfp.fileobj).readinto(pybuf) ffi.from_handle(vfp.fileobj).readinto(pybuf)
return size return size
@ffi.def_extern() @ffi.def_extern()
def _vfpWrite(vf, buffer, size): def _vfpWrite(vf, buffer, size):
vfp = ffi.cast("struct VFilePy*", vf) vfp = ffi.cast("struct VFilePy*", vf)
@ -34,19 +38,23 @@ def _vfpWrite(vf, buffer, size):
ffi.from_handle(vfp.fileobj).write(pybuf) ffi.from_handle(vfp.fileobj).write(pybuf)
return size return size
@ffi.def_extern() @ffi.def_extern()
def _vfpMap(vf, size, flags): def _vfpMap(vf, size, flags):
pass pass
@ffi.def_extern() @ffi.def_extern()
def _vfpUnmap(vf, memory, size): def _vfpUnmap(vf, memory, size):
pass pass
@ffi.def_extern() @ffi.def_extern()
def _vfpTruncate(vf, size): def _vfpTruncate(vf, size):
vfp = ffi.cast("struct VFilePy*", vf) vfp = ffi.cast("struct VFilePy*", vf)
ffi.from_handle(vfp.fileobj).truncate(size) ffi.from_handle(vfp.fileobj).truncate(size)
@ffi.def_extern() @ffi.def_extern()
def _vfpSize(vf): def _vfpSize(vf):
vfp = ffi.cast("struct VFilePy*", vf) vfp = ffi.cast("struct VFilePy*", vf)
@ -57,6 +65,7 @@ def _vfpSize(vf):
f.seek(pos, os.SEEK_SET) f.seek(pos, os.SEEK_SET)
return size return size
@ffi.def_extern() @ffi.def_extern()
def _vfpSync(vf, buffer, size): def _vfpSync(vf, buffer, size):
vfp = ffi.cast("struct VFilePy*", vf) vfp = ffi.cast("struct VFilePy*", vf)
@ -70,15 +79,14 @@ def _vfpSync(vf, buffer, size):
os.fsync() os.fsync()
return True return True
def open(f):
def open(f): # pylint: disable=redefined-builtin
handle = ffi.new_handle(f) handle = ffi.new_handle(f)
vf = VFile(lib.VFileFromPython(handle)) vf = VFile(lib.VFileFromPython(handle), _no_gc=(f, handle))
# Prevent garbage collection
vf._fileobj = f
vf._handle = handle
return vf return vf
def openPath(path, mode="r"):
def open_path(path, mode="r"):
flags = 0 flags = 0
if mode.startswith("r"): if mode.startswith("r"):
flags |= os.O_RDONLY flags |= os.O_RDONLY
@ -94,14 +102,19 @@ def openPath(path, mode="r"):
if "x" in mode[1:]: if "x" in mode[1:]:
flags |= os.O_EXCL flags |= os.O_EXCL
vf = lib.VFileOpen(path.encode("UTF-8"), flags); vf = lib.VFileOpen(path.encode("UTF-8"), flags)
if vf == ffi.NULL: if vf == ffi.NULL:
return None return None
return VFile(vf) return VFile(vf)
class VFile: class VFile:
def __init__(self, vf): def __init__(self, vf, _no_gc=None):
self.handle = vf self.handle = vf
self._no_gc = _no_gc
def __del__(self):
self.close()
def close(self): def close(self):
return bool(self.handle.close(self.handle)) return bool(self.handle.close(self.handle))
@ -112,7 +125,7 @@ class VFile:
def read(self, buffer, size): def read(self, buffer, size):
return self.handle.read(self.handle, buffer, size) return self.handle.read(self.handle, buffer, size)
def readAll(self, size=0): def read_all(self, size=0):
if not size: if not size:
size = self.size() size = self.size()
buffer = ffi.new("char[%i]" % size) buffer = ffi.new("char[%i]" % size)

View File

@ -1,2 +1,6 @@
[aliases] [aliases]
test=pytest 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 os.path
import yaml import yaml
mgba.log.installDefault(mgba.log.NullLogger()) mgba.log.install_default(mgba.log.NullLogger())
def flatten(d): def flatten(d):
l = [] l = []
@ -18,7 +18,7 @@ def flatten(d):
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
if 'vtest' in metafunc.fixturenames: 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) testList = flatten(tests)
params = [] params = []
for test in testList: for test in testList:
@ -34,9 +34,9 @@ def vtest(request):
return request.param return request.param
def test_video(vtest, pytestconfig): def test_video(vtest, pytestconfig):
vtest.setUp() vtest.setup()
if pytestconfig.getoption('--rebaseline'): if pytestconfig.getoption('--rebaseline'):
vtest.generateBaseline() vtest.generate_baseline()
else: else:
try: try:
vtest.test() vtest.test()

View File

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

View File

@ -47,7 +47,7 @@ if(NOT GIT_BRANCH)
endif() endif()
if(DEFINED PRINT_STRING) 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) elseif(NOT VERSION_STRING_CACHE OR NOT VERSION_STRING STREQUAL VERSION_STRING_CACHE)
set(VERSION_STRING_CACHE ${VERSION_STRING} CACHE STRING "" FORCE) set(VERSION_STRING_CACHE ${VERSION_STRING} CACHE STRING "" FORCE)