16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-18 00:49:19 +02:00
Files
maszyna/scripting/PyInt.cpp
2026-06-30 21:19:46 +02:00

704 lines
19 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "scripting/PyInt.h"
#include "stdafx.h"
#include "application/application.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include "utilities/dictionary.h"
#ifdef __GNUC__
#pragma GCC diagnostic ignored "-Wwrite-strings"
#endif
#include <simulation/simulation.h>
void render_task::run()
{
// convert provided input to a python dictionary
auto *input = PyDict_New();
if (input == nullptr)
{
cancel();
return;
}
for (auto const &datapair : m_input->floats)
{
auto *value{PyGetFloat(datapair.second)};
if (value == nullptr)
{
PyErr_Clear();
continue;
}
PyDict_SetItemString(input, datapair.first.c_str(), value);
Py_DECREF(value);
}
for (auto const &datapair : m_input->integers)
{
auto *value{PyLong_FromLong(datapair.second)};
if (value == nullptr)
{
PyErr_Clear();
continue;
}
PyDict_SetItemString(input, datapair.first.c_str(), value);
Py_DECREF(value);
}
for (auto const &datapair : m_input->bools)
{
// Py_True / Py_False sa niesmiertelne, ale PyDict_SetItemString i tak
// pobiera wlasna referencje - nie zwalniamy
auto *value{PyGetBool(datapair.second)};
PyDict_SetItemString(input, datapair.first.c_str(), value);
}
for (auto const &datapair : m_input->strings)
{
// Nazwy scenerii/wagonow (asName, SceneryFile, asCarName, cCode...) moga
// byc albo w UTF-8, albo w starym kodowaniu Windows-1250. PyUnicode_FromString
// wymaga POPRAWNEGO UTF-8 - dla nie-UTF-8 zwracalo NULL, a potem
// PyDict_SetItemString(dict, key, NULL) robilo Py_INCREF na NULL -> crash.
//
// Strategia: najpierw probujemy UTF-8 (strict). Jesli sie nie uda - probujemy
// cp1250. Jesli oba zawioda - pomijamy klucz, ale nie wywracamy symulatora.
char const *const str{datapair.second.c_str()};
Py_ssize_t const len{static_cast<Py_ssize_t>(datapair.second.size())};
auto *value{PyUnicode_DecodeUTF8(str, len, "strict")};
if (value == nullptr)
{
PyErr_Clear();
value = PyUnicode_Decode(str, len, "cp1250", "strict");
}
if (value == nullptr)
{
PyErr_Clear();
continue;
}
PyDict_SetItemString(input, datapair.first.c_str(), value);
Py_DECREF(value);
}
for (auto const &datapair : m_input->vec2_lists)
{
PyObject *list = PyList_New(datapair.second.size());
for (size_t i = 0; i < datapair.second.size(); i++)
{
auto const &vec = datapair.second[i];
WriteLog("passing " + glm::to_string(vec));
PyObject *tuple = PyTuple_New(2);
PyTuple_SetItem(tuple, 0, PyGetFloat(vec.x)); // steals ref
PyTuple_SetItem(tuple, 1, PyGetFloat(vec.y)); // steals ref
PyList_SetItem(list, i, tuple); // steals ref
}
PyDict_SetItemString(input, datapair.first.c_str(), list);
Py_DECREF(list);
}
m_input = nullptr;
// call the renderer
auto *output{PyObject_CallMethod(m_renderer, const_cast<char *>("render"), const_cast<char *>("O"), input)};
Py_DECREF(input);
if (output != nullptr)
{
auto *outputWidth = PyObject_CallMethod(m_renderer, const_cast<char *>("get_width"), nullptr);
auto *outputHeight = PyObject_CallMethod(m_renderer, const_cast<char *>("get_height"), nullptr);
if (outputWidth != nullptr && outputHeight != nullptr && m_target != nullptr)
{
const int screenWidth = static_cast<int>(PyLong_AsLong(outputWidth));
const int screenHeight = static_cast<int>(PyLong_AsLong(outputHeight));
const bool useRgb = false && !Global.gfx_usegles;
const int glFormat = useRgb ? GL_SRGB8 : GL_SRGB8_ALPHA8;
const int glComponents = useRgb ? GL_RGB : GL_RGBA;
const size_t bytesPerPixel = useRgb ? 3u : 4u;
const size_t expectedBytes = static_cast<size_t>(screenWidth) * static_cast<size_t>(screenHeight) * bytesPerPixel;
Py_ssize_t pythonBufferBytes = 0;
char *pythonBufferPtr = nullptr;
const bool bufferExtracted = PyBytes_AsStringAndSize(output, &pythonBufferPtr, &pythonBufferBytes) == 0 && pythonBufferPtr != nullptr;
if (!bufferExtracted)
{
ErrorLog("Python screen renderer: output is not a valid byte buffer");
}
else if (pythonBufferBytes < static_cast<Py_ssize_t>(expectedBytes))
{
ErrorLog(std::format("Python screen renderer: output buffer too small ({} bytes, expected {})", pythonBufferBytes, expectedBytes));
}
else
{
std::lock_guard guard(m_target->mutex);
if (m_target->image.size() != expectedBytes)
m_target->image.resize(expectedBytes);
std::memcpy(m_target->image.data(), pythonBufferPtr, expectedBytes);
m_target->width = screenWidth;
m_target->height = screenHeight;
m_target->components = glComponents;
m_target->format = glFormat;
m_target->timestamp = std::chrono::high_resolution_clock::now();
}
}
if (outputHeight != nullptr)
Py_DECREF(outputHeight);
if (outputWidth != nullptr)
Py_DECREF(outputWidth);
Py_DECREF(output);
}
// get commands from renderer
auto *commandsPO = PyObject_CallMethod(m_renderer, const_cast<char *>("getCommands"), nullptr);
if (commandsPO != nullptr)
{
std::vector<std::string> commands = python_external_utils::PyObjectToStringArray(commandsPO);
Py_DECREF(commandsPO);
// we perform any actions ONLY when there are any commands in buffer
if (!commands.empty())
{
for (const auto &cmd : commands)
{
std::string baseCmd;
int p1 = 0, p2 = 0;
size_t pos1 = cmd.find(';');
if (pos1 == std::string::npos)
{
baseCmd = cmd;
}
else
{
baseCmd = cmd.substr(0, pos1);
size_t pos2 = cmd.find(';', pos1 + 1);
if (pos2 == std::string::npos)
{
p1 = std::stoi(cmd.substr(pos1 + 1));
}
else
{
p1 = std::stoi(cmd.substr(pos1 + 1, pos2 - pos1 - 1));
p2 = std::stoi(cmd.substr(pos2 + 1));
}
}
auto it = simulation::commandMap.find(baseCmd);
if (it != simulation::commandMap.end())
{
command_data cd;
cd.command = it->second;
cd.action = GLFW_PRESS;
cd.param1 = p1;
cd.param2 = p2;
WriteLog("Python: Executing command [" + baseCmd + "] with params: P1=" + std::to_string(p1) + " P2=" + std::to_string(p2) +
" Target ID=" + std::to_string(simulation::Train->id()));
simulation::Commands.push(cd, static_cast<size_t>(command_target::vehicle) | simulation::Train->id());
}
else
{
ErrorLog("Python: Command [" + baseCmd + "] not found!");
}
}
}
}
}
void render_task::upload()
{
if (Global.python_uploadmain && m_target && m_target->shared_tex)
{
m_target->shared_tex->update_from_memory(m_target->width, m_target->height, reinterpret_cast<const uint8_t *>(m_target->image.data()));
}
}
void render_task::cancel() {}
// initializes the module. returns true on success
auto python_taskqueue::init() -> bool
{
crashreport_add_info("python.threadedupload", Global.python_threadedupload ? "yes" : "no");
crashreport_add_info("python.uploadmain", Global.python_uploadmain ? "yes" : "no");
#ifdef _WIN32
const wchar_t *pythonhome = sizeof(void *) == 8 ? L"python64" : L"python";
#elif __linux__
const wchar_t *pythonhome = (sizeof(void *) == 8) ? L"linuxpython64" : L"linuxpython";
#elif __APPLE__
const wchar_t *pythonhome = (sizeof(void *) == 8) ? L"macpython64" : L"macpython";
#endif
{
// Py_SetPythonHome / Py_InitializeEx are deprecated since Python 3.11.
// Use the PyConfig init API (PEP 587) instead.
PyConfig config;
PyConfig_InitPythonConfig(&config);
config.install_signal_handlers = 0; // matches former Py_InitializeEx(0)
PyStatus status = PyConfig_SetString(&config, &config.home, pythonhome);
if (PyStatus_Exception(status))
{
PyConfig_Clear(&config);
ErrorLog("Python Interpreter: failed to set PYTHONHOME");
return false;
}
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status))
{
std::string msg = "Python Interpreter: Py_InitializeFromConfig failed";
if (status.err_msg != nullptr)
msg += std::string(": ") + status.err_msg;
if (status.func != nullptr)
msg += std::string(" (in ") + status.func + ")";
PyConfig_Clear(&config);
ErrorLog(msg);
return false;
}
PyConfig_Clear(&config);
}
PyObject *stringiomodule{nullptr};
PyObject *stringioclassname{nullptr};
PyObject *stringioobject{nullptr};
// do the setup work while we hold the lock
m_main = PyImport_ImportModule("__main__");
if (m_main == nullptr)
{
ErrorLog("Python Interpreter: __main__ module is missing");
goto release_and_exit;
}
stringiomodule = PyImport_ImportModule("io");
stringioclassname = stringiomodule != nullptr ? PyObject_GetAttrString(stringiomodule, "StringIO") : nullptr;
stringioobject = stringioclassname != nullptr ? PyObject_CallObject(stringioclassname, nullptr) : nullptr;
m_stderr = {(stringioobject == nullptr ? nullptr : PySys_SetObject(const_cast<char *>("stderr"), stringioobject) != 0 ? nullptr : stringioobject)};
if (false == run_file("abstractscreenrenderer"))
{
goto release_and_exit;
}
// release the lock, save the state for future use
m_mainthread = PyEval_SaveThread();
WriteLog("Python Interpreter: setup complete");
// init workers
for (auto &worker : m_workers)
{
GLFWwindow *openglcontextwindow = nullptr;
if (Global.python_threadedupload)
openglcontextwindow = Application.window(-1);
worker = std::jthread(&python_taskqueue::run, this, openglcontextwindow, std::ref(m_tasks), std::ref(m_uploadtasks), std::ref(m_condition), std::ref(m_exit));
if (false == worker.joinable())
{
return false;
}
}
m_initialized = true;
return true;
release_and_exit:
PyEval_SaveThread();
return false;
}
// shuts down the module
void python_taskqueue::exit()
{
if (!m_initialized)
return;
// let the workers know we're done with them
m_exit = true;
m_condition.notify_all();
// let them free up their shit before we proceed
m_workers = {};
// drop any pending render/upload tasks. the workers are joined at this
// point so the locks are strictly defensive, but they cost nothing and
// document intent. clearing the deque also actually releases the tasks,
// which the previous code did not do (cancel() is a no-op stub).
{
std::lock_guard<std::mutex> lock(m_tasks.mutex);
for (auto &task : m_tasks.data)
{
task->cancel();
}
m_tasks.data.clear();
}
{
std::lock_guard<std::mutex> lock(m_uploadtasks.mutex);
m_uploadtasks.data.clear();
}
// reclaim cached python objects while the interpreter is still alive,
// so no Py_DECREF lands on a finalized interpreter during later teardown
acquire_lock();
for (auto &entry : m_renderers)
{
Py_XDECREF(entry.second);
}
m_renderers.clear();
Py_XDECREF(m_stderr);
m_stderr = nullptr;
Py_XDECREF(m_main);
m_main = nullptr;
// take a bow
Py_Finalize();
m_initialized = false;
}
// adds specified task along with provided collection of data to the work queue. returns true on success
auto python_taskqueue::insert(task_request const &Task) -> bool
{
if (!m_initialized || false == Global.python_enabled || Task.renderer.empty() || Task.input == nullptr || Task.target == 0)
{
return false;
}
auto *renderer{fetch_renderer(Task.renderer)};
if (renderer == nullptr)
{
return false;
}
auto newtask = std::make_shared<render_task>(renderer, Task.input, Task.target);
bool newtaskinserted{false};
// acquire a lock on the task queue and add the new task
{
std::lock_guard<std::mutex> lock(m_tasks.mutex);
// check the task list for a pending request with the same target
for (auto &task : m_tasks.data)
{
if (task->target() == Task.target)
{
// replace pending task in the slot with the more recent one
task->cancel();
task = newtask;
newtaskinserted = true;
break;
}
}
if (false == newtaskinserted)
{
m_tasks.data.emplace_back(newtask);
}
}
// potentially wake a worker to handle the new task
m_condition.notify_one();
// all done
return true;
}
// executes python script stored in specified file. returns true on success
auto python_taskqueue::run_file(std::string const &File, std::string const &Path) -> bool
{
auto const lookup{FileExists({Path + File, "python/local/" + File}, {".py"})};
if (lookup.first.empty())
{
return false;
}
std::ifstream inputfile{lookup.first + lookup.second};
std::string input;
input.assign(std::istreambuf_iterator<char>(inputfile), std::istreambuf_iterator<char>());
if (PyRun_SimpleString(input.c_str()) != 0)
{
error();
return false;
}
return true;
}
// acquires the python gil and sets the main thread as current
void python_taskqueue::acquire_lock()
{
PyEval_RestoreThread(m_mainthread);
}
// releases the python gil and swaps the main thread out
void python_taskqueue::release_lock()
{
PyEval_SaveThread();
}
auto python_taskqueue::fetch_renderer(std::string const Renderer) -> PyObject *
{
auto const lookup{m_renderers.find(Renderer)};
if (lookup != std::end(m_renderers))
{
return lookup->second;
}
// try to load specified renderer class
std::string const path{substr_path(Renderer)};
auto const file{Renderer.substr(path.size())};
PyObject *renderer{nullptr};
PyObject *rendererarguments{nullptr};
PyObject *renderername{nullptr};
acquire_lock();
{
if (m_main == nullptr)
{
ErrorLog("Python Renderer: __main__ module is missing");
goto cache_and_return;
}
if (false == run_file(file, path))
{
goto cache_and_return;
}
renderername = PyObject_GetAttrString(m_main, file.c_str());
if (renderername == nullptr)
{
ErrorLog("Python Renderer: class \"" + file + "\" not defined");
goto cache_and_return;
}
rendererarguments = Py_BuildValue("(s)", path.c_str());
if (rendererarguments == nullptr)
{
ErrorLog("Python Renderer: failed to create initialization arguments");
goto cache_and_return;
}
renderer = PyObject_CallObject(renderername, rendererarguments);
PyObject_CallMethod(renderer, const_cast<char *>("manul_set_format"), const_cast<char *>("(s)"), "RGBA");
if (PyErr_Occurred() != nullptr)
{
error();
renderer = nullptr;
}
cache_and_return:
// clean up after yourself
if (rendererarguments != nullptr)
{
Py_DECREF(rendererarguments);
}
}
release_lock();
// cache the failures as well so we don't try again on subsequent requests
m_renderers.emplace(Renderer, renderer);
return renderer;
}
void python_taskqueue::run(GLFWwindow *Context, rendertask_sequence &Tasks, uploadtask_sequence &Upload_Tasks, threading::condition_variable &Condition, std::atomic<bool> &Exit)
{
if (Context)
glfwMakeContextCurrent(Context);
// create a state object for this thread
PyEval_AcquireThread(m_mainthread);
auto *threadstate{PyThreadState_New(m_mainthread->interp)};
PyEval_ReleaseThread(m_mainthread);
std::shared_ptr<render_task> task{nullptr};
while (false == Exit.load())
{
// regardless of the reason we woke up prime the spurious wakeup flag for the next time
Condition.spurious(true);
// keep working as long as there's any scheduled tasks
do
{
task = nullptr;
// acquire a lock on the task queue and potentially grab a task from it
{
std::lock_guard<std::mutex> lock(Tasks.mutex);
if (false == Tasks.data.empty())
{
// fifo
task = Tasks.data.front();
Tasks.data.pop_front();
}
}
if (task != nullptr)
{
// swap in my thread state
PyEval_RestoreThread(threadstate);
{
// execute python code
task->run();
if (Context)
task->upload();
else
{
std::lock_guard<std::mutex> lock(Upload_Tasks.mutex);
Upload_Tasks.data.push_back(task);
}
if (PyErr_Occurred() != nullptr)
error();
}
// clear the thread state
PyEval_SaveThread();
}
// TBD, TODO: add some idle time between tasks in case we're on a single thread cpu?
} while (task != nullptr);
// if there's nothing left to do wait until there is
// short timeout: notify_all() in exit() can race with the worker's
// transition into the wait, and the run loop also drives prompt
// shutdown checks
Condition.wait_for(std::chrono::milliseconds(250));
}
// clean up thread state data
// Re-acquire the GIL with this worker's thread state, then clear and delete it.
// PyThreadState_DeleteCurrent() frees the current thread state AND releases the GIL,
// so we must NOT call PyEval_SaveThread() afterwards: at that point the current
// thread state is already NULL and PyEval_SaveThread() fatals on a NULL tstate.
PyEval_RestoreThread(threadstate);
PyThreadState_Clear(threadstate);
PyThreadState_DeleteCurrent();
// detach the GL context before the worker terminates; some drivers
// (NVIDIA on X11, certain Mesa/Wayland configs) hang in process teardown
// if a context is still current on a dying thread
if (Context)
glfwMakeContextCurrent(nullptr);
}
void python_taskqueue::update()
{
std::lock_guard<std::mutex> lock(m_uploadtasks.mutex);
for (auto &task : m_uploadtasks.data)
task->upload();
m_uploadtasks.data.clear();
}
void python_taskqueue::error()
{
if (m_stderr != nullptr)
{
// std err pythona jest buforowane
PyErr_Print();
auto *errortext{PyObject_CallMethod(m_stderr, const_cast<char *>("getvalue"), nullptr)};
if (errortext != nullptr)
{
const char *errstr = PyUnicode_AsUTF8(errortext);
if (errstr != nullptr)
ErrorLog(errstr);
Py_DECREF(errortext);
}
PyObject_CallMethod(m_stderr, const_cast<char *>("truncate"), const_cast<char *>("L"), (long long)0);
PyObject_CallMethod(m_stderr, const_cast<char *>("seek"), const_cast<char *>("L"), (long long)0);
}
else
{
// nie dziala buffor pythona
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
if (type == nullptr)
{
ErrorLog("Python Interpreter: don't know how to handle null exception");
}
PyErr_NormalizeException(&type, &value, &traceback);
if (type == nullptr)
{
ErrorLog("Python Interpreter: don't know how to handle null exception");
}
auto *typetext{PyObject_Str(type)};
if (typetext != nullptr)
{
const char *s = PyUnicode_AsUTF8(typetext);
if (s)
ErrorLog(s);
Py_DECREF(typetext);
}
if (value != nullptr)
{
auto *valuetext{PyObject_Str(value)};
if (valuetext != nullptr)
{
const char *s = PyUnicode_AsUTF8(valuetext);
if (s)
ErrorLog(s);
Py_DECREF(valuetext);
}
}
auto *tracebacktext{PyObject_Str(traceback)};
if (tracebacktext != nullptr)
{
const char *s = PyUnicode_AsUTF8(tracebacktext);
if (s)
ErrorLog(s);
Py_DECREF(tracebacktext);
}
else
{
WriteLog("Python Interpreter: failed to retrieve the stack traceback");
}
}
}
std::vector<std::string> python_external_utils::PyObjectToStringArray(PyObject *pyList)
{
std::vector<std::string> result;
std::vector<std::string> emptyIfError = {};
if (!PySequence_Check(pyList))
{
ErrorLog("Python: Failed to convert PyObject -> vector<string>");
return emptyIfError;
}
Py_ssize_t size = PySequence_Size(pyList);
for (Py_ssize_t i = 0; i < size; ++i)
{
PyObject *item = PySequence_GetItem(pyList, i); // Increments reference count
if (item == nullptr)
{
ErrorLog("Python: Failed to get item from sequence.");
return emptyIfError;
}
const char *str = PyUnicode_AsUTF8(item);
if (str == nullptr)
{
Py_DECREF(item);
ErrorLog("Python: Failed to convert item to string.");
return emptyIfError;
}
result.push_back(std::string(str));
Py_DECREF(item); // Decrease reference count for the item
}
return result;
}
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif