Skip to content
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

Draft
wants to merge 64 commits into
base: master
Choose a base branch
from
Draft

Conversation

Felk
Copy link
Contributor

@Felk Felk commented Jun 3, 2018

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:

from dolphin import event, memory

# framecount in Pokemon Battle Revolution
addr = 0x8063fc2c

def onframeadvance():
    value = memory.read_u32(addr)
    print(f"Internal framecount: {value}")

event.onframeadvance(onframeadvance)

Minimal scope goals:

  • put the python external into a submodule and add a readme on how to collect the required files from scratch for maintenance.
  • add basic memory reading/writing support
  • add controller input support
  • add basic support for on-screen drawing
  • add savestate support
  • add frameadvance event
  • command line flag for automatic start of a script

The scripting API surface shall be implemented as their need arises and this get more feedback.

Polishing goals:

  • Linux support
  • MacOS support
  • add GUI for running scripts.
  • no global state in python implementation (use of sub-interpreters)
  • extend API surface to something that would be perceived as servicable and not just "minimal". I don't have a concrete list of what that would contain.

Open for discussion:

  • Is Python a good choice? Should default scripting support be in LUA or something else instead? If Python is OK, then:
    • what about the additional file size? In its current form it would add ~4.7 MB to the zipped file.
    • should it be disabled (as in, not compiled) by default?

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.

@leoetlino leoetlino added WIP / do not merge Work in progress (do not merge) RFC Request for comments labels Jun 3, 2018
Copy link
Member

@lioncash lioncash left a 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.


s8 HostRead_S8 (const u32 address)
{
return Common::BitCast<s8>(HostRead<u8> (address));

This comment was marked as off-topic.

@@ -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.


void HostWrite_S8 (const s8 var, const u32 address)
{
HostWrite<u8> (Common::BitCast<u8>(var), address);

This comment was marked as off-topic.

@@ -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.

void Init();
void Shutdown();

typedef void (*Task)();

This comment was marked as off-topic.

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.

@@ -0,0 +1,12 @@
#pragma once

This comment was marked as off-topic.

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.

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.

@@ -0,0 +1,57 @@
#pragma once

This comment was marked as off-topic.

Copy link
Member

@lioncash lioncash left a 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.

@@ -0,0 +1,23 @@

#include "Events.h"

This comment was marked as off-topic.


// 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.

template <typename T>
void EmitEvent(T evt)
{
for (Listener<T> listener : GetListenersVector<T>())

This comment was marked as off-topic.

template <typename T>
void ListenEvent(Listener<T> listener)
{
GetListenersVector<T>().push_back(listener);

This comment was marked as off-topic.

@Warepire
Copy link

Warepire commented Jun 4, 2018

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.

@Felk
Copy link
Contributor Author

Felk commented Jun 4, 2018

@lioncash

I'm very not fond of the way many of the modules are file-scope based and not class-based

Regarding the python module functions, they unfortunately require me to declare them as typedef PyObject *(*PyCFunction)(PyObject *, PyObject *);. 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.

Copy link
Member

@lioncash lioncash left a 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 have s_ on them. Globals with external linkage should have a g_ 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 of GetId()). 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.


namespace PyScripting
{
inline const char* pycode = R"(

This comment was marked as off-topic.

{
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.

{
return PyFloat_FromDouble(n);
}
else if constexpr (std::is_integral<T>::value)

This comment was marked as off-topic.

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.

template <typename T>
constexpr const char* GetPyFormat()
{
if constexpr (std::is_same<T, signed char>::value)

This comment was marked as off-topic.

@@ -1,595 +1,616 @@
<?xml version="1.0" encoding="utf-8"?>

This comment was marked as off-topic.

@@ -1,1587 +1,1622 @@
<?xml version="1.0" encoding="utf-8"?>

This comment was marked as off-topic.

@Felk
Copy link
Contributor Author

Felk commented Jun 5, 2018

@lioncash

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.
If the scripting is in its own little project, would you still want it to allow for multiple instances? Or would that be okay roughly the way it is now?

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

@lioncash
Copy link
Member

lioncash commented Jun 6, 2018

I can try doing that for this PR already if you want.

That would actually be fantastic.

If the scripting is in its own little project, would you still want it to allow for multiple instances?

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).

@Felk Felk force-pushed the scripting branch 2 times, most recently from 234941a to 3de7ac3 Compare June 6, 2018 23:11
@Felk
Copy link
Contributor Author

Felk commented Jul 18, 2018

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:

  • the python scripting project will just have static methods for now. To fully avoid global state I'll need to dive deeper into how subinterpreters work eventually.
  • No UI for selecting scripts. Not gonna open a whole new can of worms, that is QT. Instead, I'll just add a command line parameter for now.
  • I will collect the functionality offered to scripting in the API namespace I already created, but make no attempts trying to finetune it for anything else. This means disregarding the planned "universal dolphin API" thing. I do want it to be a good start to build off of though if someone does put effort into such an API eventually.
  • Support for not-Windows. This is just a matter of either finally diving into CMake, or getting help from someone to recreate the build steps I added for msbuild

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.

@Felk Felk force-pushed the scripting branch 2 times, most recently from aa20c28 to c6a4394 Compare July 18, 2018 20:43
@Felk
Copy link
Contributor Author

Felk commented Aug 16, 2019

sorry, this is dead and it's unlikely I'll ever pick it up again and finish

@Felk Felk closed this Aug 16, 2019
@Felk
Copy link
Contributor Author

Felk commented Aug 4, 2020

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.

@Felk
Copy link
Contributor Author

Felk commented Nov 20, 2020

As well as this, the code contained for the scripting API uses ImGUI, but the code is not well separated from Core and as such ImGUI now needs to be linked to the Core section of code in order for it to build (which isn't ideal in certain build circumstances eg headless, libretro etc)

@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?

@AlexApps99
Copy link
Contributor

AlexApps99 commented Nov 20, 2020

That is a good point you make.
I am not very experienced with Dolphin's codebase, so it's possible it's not an issue after all.

I will try to contact a dev to see if they agree.
Thanks for bringing this up

Edit: I talked to a dev yesterday, but they were unsure.
I suppose we could ignore the issue for now and address it later when another dev can evaluate it

@AlexApps99
Copy link
Contributor

@Felk do you still have plans to work on this? Totally fine if not, just wondering

@Felk
Copy link
Contributor Author

Felk commented Apr 12, 2021

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.

@AlexApps99
Copy link
Contributor

Ok, that's good to hear :)

Felk added 16 commits October 21, 2021 18:58
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,
Copy link
Contributor

@Zopolis4 Zopolis4 Jun 8, 2022

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.

Copy link
Contributor Author

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
RFC Request for comments WIP / do not merge Work in progress (do not merge)
Development

Successfully merging this pull request may close these issues.

8 participants