-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Add Python Scripting support #7064
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some comments.
I'm very not fond of the way many of the modules are file-scope based and not class-based. This means program execution has to wait until all of those objects are constructed before anything else can be done, which is pretty wasteful if a person has no intention of even using scripting. If someone doesn't use scripting, they shouldn't have resource overhead just from it merely existing.
Source/Core/Core/PowerPC/MMU.cpp
Outdated
|
||
s8 HostRead_S8 (const u32 address) | ||
{ | ||
return Common::BitCast<s8>(HostRead<u8> (address)); |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
Source/Core/Core/PowerPC/MMU.cpp
Outdated
@@ -555,24 +577,50 @@ void Write_F64(const double var, const u32 address) | |||
Write_U64(integral, address); | |||
} | |||
|
|||
u8 HostRead_U8(const u32 address) | |||
template<typename T> | |||
T HostRead(const u32 address) |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
Source/Core/Core/PowerPC/MMU.cpp
Outdated
|
||
void HostWrite_S8 (const s8 var, const u32 address) | ||
{ | ||
HostWrite<u8> (Common::BitCast<u8>(var), address); |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
Source/Core/Core/PowerPC/MMU.cpp
Outdated
@@ -589,24 +637,47 @@ double HostRead_F64(const u32 address) | |||
return Common::BitCast<double>(integral); | |||
} | |||
|
|||
void HostWrite_U8(const u8 var, const u32 address) | |||
template<typename T> | |||
void HostWrite(const T var, const u32 address) |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
void Init(); | ||
void Shutdown(); | ||
|
||
typedef void (*Task)(); |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
bool ParseTupleSimple(PyObject* args, T1* arg1, T2* arg2) | ||
{ | ||
const std::string format = | ||
std::string(Pyr::get_pyformat<T1>()) + std::string(Pyr::get_pyformat<T2>()); |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
@@ -0,0 +1,12 @@ | |||
#pragma once |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
UTF8ToUTF16(File::GetExeDirectory()) + L"/python36-embed;" + | ||
UTF8ToUTF16(File::GetExeDirectory()); // TODO can this be done more elegantly? | ||
|
||
static std::deque<Task> tasks; |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
UTF8ToUTF16(File::GetExeDirectory()); // TODO can this be done more elegantly? | ||
|
||
static std::deque<Task> tasks; | ||
static CoreTiming::EventType* scripting_event = nullptr; |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
@@ -0,0 +1,57 @@ | |||
#pragma once |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Realized I didn't comment on Event.h/Event.cpp, whoops.
Source/Core/Core/API/Events.cpp
Outdated
@@ -0,0 +1,23 @@ | |||
|
|||
#include "Events.h" |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
Source/Core/Core/API/Events.h
Outdated
|
||
// a listener on T is any function that takes a const reference to T as argument | ||
template <typename T> | ||
using Listener = void (*)(const T&); |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
Source/Core/Core/API/Events.h
Outdated
template <typename T> | ||
void EmitEvent(T evt) | ||
{ | ||
for (Listener<T> listener : GetListenersVector<T>()) |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
Source/Core/Core/API/Events.h
Outdated
template <typename T> | ||
void ListenEvent(Listener<T> listener) | ||
{ | ||
GetListenersVector<T>().push_back(listener); |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
For feedback on your question regarding Python vs Lua by users, one user group will be the people at TASVideos. Currently as you saw most emulators use Lua, but I do not think Python is completely out of the picture (personally I'm a bigger fan of Python). Pop by their IRC or start a discussion on the forums to gather data. |
Regarding the python module functions, they unfortunately require me to declare them as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Felk re: functions in a list std::function instances have a .target()
function which retrieves the address of the stored callable object. It may be worthwhile adding a better means of identifying the functions however (it wouldn't be wise to rely on anonymous function/lambda addresses).
For all the other stuff, I could put it in classes, but those will effectively be singletons (or at least not work with multiple instances), if that's what you mean.
Note that this throws a massive wrench in the plans I was slowly working towards over time which was eliminating most mutable global state (yes, even if it's static
and file scope, it's still considered a global as far as the language cares) in order to allow multiple instances of the core to be run in the same process, which would be preferable for making the core a proper library in the future. It's also worth noting that one of the requirements I urged should be met for the implementation was "no global state" (where avoidable), and on closer inspection of the current implementation I see this is... really kind of not the case. Scripting is inherently driven from the frontend for the core, so why is it not situated around the frontends initializing the scripting? (currently it's grafted into Core.cpp). I remember @delroth bringing up ideas related to scripting that would also be usable UI-side (I can't remember off the top of my head what they were though).
Again, a frontend shouldn't have overhead for something that isn't being used at the time by someone, and when something is finished being used, those resources shouldn't stick around, currently this operates orthogonal to that.
Aside from that, why are most of the modules implemented as headers as opposed to having a paired .cpp file with it? Many of these have static
variables in them, which can cause odd issues related to addresses (as every translation unit gets their own instance of those static variables). So an accidental usage of something in a source file can lead to difficult to debug situations. It also means any change to implementation code requires rebuilding everything that includes it. Not a big con, but it still exists.
Convention-wise:
- All private class variables should have an
m_
prefix, and file-static variables should haves_
on them. Globals with external linkage should have ag_
on them, but I really hope that doesn't become necessary. - Public interface should come before private interface. Public interface is what people are going to be reading and using primarily. It's not useful to put things devs shouldn't care about 99% of the time as the first thing in a class body.
- All functions in the codebase should follow,
ThisIsAName()
(where initialisms are upper-cased, for example,GetID()
instead ofGetId()
). This doesn't mean the actual scripting API has to be like that however. Conventions for that haven't even been discussed, I think (which probably should be at some point, if that's the case, and documented somewhere).
char* x; | ||
if (!PyrArg::ParseTupleSimple(args, &x)) | ||
return nullptr; | ||
if (std::string(x) == "frameadvance") |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
|
||
namespace PyScripting | ||
{ | ||
inline const char* pycode = R"( |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
{ | ||
return PyBool_FromLong(n); | ||
} | ||
else if constexpr (std::is_floating_point<T>::value) |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
{ | ||
return PyFloat_FromDouble(n); | ||
} | ||
else if constexpr (std::is_integral<T>::value) |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
else if constexpr (std::is_integral<T>::value) | ||
{ | ||
static_assert(sizeof(T) <= sizeof(unsigned long long), "integral type too big, not supported"); | ||
const bool is_signed = std::is_signed<T>::value; |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
template <typename T> | ||
constexpr const char* GetPyFormat() | ||
{ | ||
if constexpr (std::is_same<T, signed char>::value) |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
Source/Core/Core/Core.vcxproj
Outdated
@@ -1,595 +1,616 @@ | |||
<?xml version="1.0" encoding="utf-8"?> |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
@@ -1,1587 +1,1622 @@ | |||
<?xml version="1.0" encoding="utf-8"?> |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
regarding global state: Python can run multiple "sub-interpreters", so it's definetely possible to get it to work in class scope, with some more effort. The exception to this still are the python extension functions though. Your idea of getting it out of the core sounds even better though, as I don't plan on having the rest of the core be coupled to the scripting anyway, and instead go over that API-namespace. That part can probably be put into classes without trouble. I can try doing that for this PR already if you want. regarding header-only files: That's just me being lazy with the still prototype-y code. I'll clean that up. regarding all the code reviews: I'll try to put code I consider ready for review into dedicated commits, so all the WIP-code is situated in the most recent commit and you don't have to look at code I already know I will change soon |
That would actually be fantastic.
This would make it considerably more flexible in terms of ways it can be used (and in terms of how the rest of the codebase can change around it in lifecycle-related means). |
234941a
to
3de7ac3
Compare
Sorry I have not worked much on this recently. I plan on getting a minimal solution done which basically contains scripting support for memory editing, input, basic on-screen drawing, and some events. Because I have never done a lot of these things, I don't want to be too ambitious and risk my little project here completely perishing. For that reason, I postponed the following things:
Even if after that point it's not ready for a merge (it won't be), I still want to "publish" it to a few people (annoy a few tas'ers, dolphin devs, tpp devs, and myself; reimplementing that pbrengine thing is still a goal of mine) to get early feedback and refine the minimal scope. Polishing (or scrapping the whole project, who knows) comes after. I edited the original post accordingly to track these goals. |
aa20c28
to
c6a4394
Compare
sorry, this is dead and it's unlikely I'll ever pick it up again and finish |
Assuming that I did continue to work on this branch, and got it to a working state that also addressed some of the original issues (like no global state), would you want to take another look, even if there is no intent (in the short term at least) to actually merge it? Basically I'm asking for permission to waste your time looking over it and to re-open this as WIP with the possibility of it staying a fork anyway. |
@AlexApps99 I tried to understand the issue more deeply and got confused: It seems that Core needs to always link against VideoCommon anways (e.g. for OnScreenDisplay), and VideoCommon already depends on ImGui. Can you please explain a scenario to me that I can reproduce where removing the ImGui dependency from the Core would result in something that does not depend on ImGui in other places? |
That is a good point you make. I will try to contact a dev to see if they agree. Edit: I talked to a dev yesterday, but they were unsure. |
add linux support
@Felk do you still have plans to work on this? Totally fine if not, just wondering |
I do, but unfortunately I usually program for a couple of weeks and then completely lose interest for months or years. That's not deliberate, I haven't abandoned this, my working habits are just very unpredictable. |
Ok, that's good to hear :) |
This fixes a follow-up of 7a6b63b "MMU: Don't truncate 64-bit values when calling Memcheck()".
…is drawn I believe (haven't tested) that this is different to how e.g BizHawk does it. Their Lua documentation for events mentions `event.onframeend` to happen "after all emulation and drawing has completed" and as the "'default behavior of lua scripts". However, doing it this way instead feels more intuitive for the default behavior, because otherwise everything that happens between frames can only be observed after the frame has just been drawn, requiring one more frame to pass before being able to display anything using e.g. `gui.draw_text`.
Previously there was no check if a button was omitted in the button map, and the emulator just crashed. Now omitting button keys is allowed and will assume to the unpressed/default state.
in the ISO list and selecting "ISO Properties". | ||
There is nothing fundamentally stopping this to work on ARM as well, | ||
but currently only the Python [externals](Externals) for Windows x86-64 are bundled. | ||
There are no embeddable Python distribution for ARM64 readily available on python.org, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The latest 3.11 prereleases have ARM64 embeddable packages for Windows, so unless 3.11 somehow breaks compatibility that's something to consider when 3.11 gets a stable release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, that's good to know, thanks!
Scripting support is already functional, but still being worked on to get to a mergable state. You can try out the most recent state over at my fork at felk/dolphin. See its readme for further instructions.
I am currently in the process of adding scripting support in the form of embedded python scripts, which will work in a simple event-callback-based fashion. A script can basially register callbacks on events at startup, and also call back into functions defined by dolphin, e.g. reading memory etc., similar to how it's done in other emulators like Bizhawk or vba. I also added some
async/await
magic, but that's just convenience specific to python I like.A script might look like this:
Minimal scope goals:
The scripting API surface shall be implemented as their need arises and this get more feedback.
Polishing goals:
Open for discussion:
The decoupling between the scripting implementation and interfacing to the core is going over an
API
namespace proxying commands to the core and decoupling events. Have a graph of how it roughly looks design-wise here.